@imbingox/acex 0.1.0 → 0.3.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +96 -285
  2. package/index.ts +1 -0
  3. package/package.json +40 -23
  4. package/src/adapters/binance/adapter.ts +80 -0
  5. package/src/adapters/binance/book-ticker.ts +123 -0
  6. package/src/adapters/binance/mark-price.ts +126 -0
  7. package/src/adapters/binance/market-catalog.ts +258 -0
  8. package/src/adapters/binance/private-adapter.ts +833 -0
  9. package/src/adapters/types.ts +219 -0
  10. package/src/client/context.ts +123 -0
  11. package/src/client/create-client.ts +6 -0
  12. package/src/client/private-subscription-coordinator.ts +512 -0
  13. package/src/client/runtime.ts +410 -0
  14. package/src/errors.ts +27 -0
  15. package/src/index.ts +5 -0
  16. package/src/internal/async-event-bus.ts +100 -0
  17. package/src/internal/filters.ts +117 -0
  18. package/src/internal/managed-websocket.ts +280 -0
  19. package/src/managers/account-manager.ts +609 -0
  20. package/src/managers/market-manager.ts +912 -0
  21. package/src/managers/order-manager.ts +685 -0
  22. package/src/types/account.ts +157 -0
  23. package/src/types/client.ts +79 -0
  24. package/src/types/index.ts +5 -0
  25. package/src/types/market.ts +152 -0
  26. package/src/types/order.ts +177 -0
  27. package/src/types/shared.ts +93 -0
  28. package/dist/adapters/binance/composite-adapter.d.ts +0 -116
  29. package/dist/adapters/binance/composite-adapter.js +0 -121
  30. package/dist/adapters/binance/market-types.d.ts +0 -63
  31. package/dist/adapters/binance/market-types.js +0 -1
  32. package/dist/adapters/binance/native-market-adapter.d.ts +0 -102
  33. package/dist/adapters/binance/native-market-adapter.js +0 -455
  34. package/dist/adapters/binance/normalizers.d.ts +0 -8
  35. package/dist/adapters/binance/normalizers.js +0 -123
  36. package/dist/adapters/binance/rest-client.d.ts +0 -17
  37. package/dist/adapters/binance/rest-client.js +0 -66
  38. package/dist/adapters/binance/symbol-router.d.ts +0 -9
  39. package/dist/adapters/binance/symbol-router.js +0 -174
  40. package/dist/adapters/binance/ws-client.d.ts +0 -24
  41. package/dist/adapters/binance/ws-client.js +0 -261
  42. package/dist/adapters/ccxt/aster-ccxt-adapter.d.ts +0 -157
  43. package/dist/adapters/ccxt/aster-ccxt-adapter.js +0 -272
  44. package/dist/adapters/ccxt/binance-usdm-ccxt-adapter.d.ts +0 -180
  45. package/dist/adapters/ccxt/binance-usdm-ccxt-adapter.js +0 -539
  46. package/dist/adapters/ccxt/binance-usdm-exchange.d.ts +0 -22
  47. package/dist/adapters/ccxt/binance-usdm-exchange.js +0 -23
  48. package/dist/adapters/fake/fake-aster-adapter.d.ts +0 -130
  49. package/dist/adapters/fake/fake-aster-adapter.js +0 -283
  50. package/dist/adapters/types.d.ts +0 -210
  51. package/dist/adapters/types.js +0 -1
  52. package/dist/core/client.d.ts +0 -50
  53. package/dist/core/client.js +0 -403
  54. package/dist/core/recovery.d.ts +0 -22
  55. package/dist/core/recovery.js +0 -18
  56. package/dist/core/runtime.d.ts +0 -26
  57. package/dist/core/runtime.js +0 -150
  58. package/dist/errors/acex-error.d.ts +0 -25
  59. package/dist/errors/acex-error.js +0 -54
  60. package/dist/index.d.ts +0 -6
  61. package/dist/index.js +0 -3
  62. package/dist/managers/account-manager.d.ts +0 -41
  63. package/dist/managers/account-manager.js +0 -80
  64. package/dist/managers/market-manager.d.ts +0 -16
  65. package/dist/managers/market-manager.js +0 -28
  66. package/dist/managers/order-manager.d.ts +0 -87
  67. package/dist/managers/order-manager.js +0 -122
  68. package/dist/runtime/async-queue.d.ts +0 -8
  69. package/dist/runtime/async-queue.js +0 -88
  70. package/dist/runtime/request-id.d.ts +0 -1
  71. package/dist/runtime/request-id.js +0 -5
  72. package/dist/runtime/ws-connection-supervisor.d.ts +0 -76
  73. package/dist/runtime/ws-connection-supervisor.js +0 -522
  74. package/dist/store/account-store.d.ts +0 -52
  75. package/dist/store/account-store.js +0 -18
  76. package/dist/store/health-store.d.ts +0 -16
  77. package/dist/store/health-store.js +0 -29
  78. package/dist/store/market-store.d.ts +0 -42
  79. package/dist/store/market-store.js +0 -51
  80. package/dist/store/order-store.d.ts +0 -38
  81. package/dist/store/order-store.js +0 -49
  82. package/dist/testing/create-fake-runtime.d.ts +0 -5
  83. package/dist/testing/create-fake-runtime.js +0 -7
  84. package/dist/types/public.d.ts +0 -5
  85. package/dist/types/public.js +0 -1
@@ -0,0 +1,512 @@
1
+ import type {
2
+ PrivateUserDataAdapter,
3
+ StreamHandle,
4
+ } from "../adapters/types.ts";
5
+ import { AcexError } from "../errors.ts";
6
+ import type { AccountRuntimeOptions, Exchange } from "../types/index.ts";
7
+ import type {
8
+ ClientContext,
9
+ PrivateAccountDataConsumer,
10
+ PrivateOrderDataConsumer,
11
+ RegisteredAccountRecord,
12
+ } from "./context.ts";
13
+
14
+ interface PrivateSubscriptionRecord {
15
+ accountId: string;
16
+ exchange: Exchange;
17
+ accountSubscribed: boolean;
18
+ ordersSubscribed: boolean;
19
+ accountReady: boolean;
20
+ orderReady: boolean;
21
+ stream?: StreamHandle;
22
+ startPromise?: Promise<void>;
23
+ reconcilePromise?: Promise<void>;
24
+ }
25
+
26
+ const DEFAULT_STREAM_OPEN_TIMEOUT_MS = 15_000;
27
+ const DEFAULT_STREAM_RECONNECT_DELAY_MS = 1_000;
28
+ const DEFAULT_STREAM_RECONNECT_MAX_DELAY_MS = 10_000;
29
+ const DEFAULT_LISTEN_KEY_KEEPALIVE_MS = 30 * 60 * 1_000;
30
+
31
+ export class PrivateSubscriptionCoordinator {
32
+ private readonly context: ClientContext;
33
+ private readonly adapter: PrivateUserDataAdapter;
34
+ private readonly accountConsumer: PrivateAccountDataConsumer;
35
+ private readonly orderConsumer: PrivateOrderDataConsumer;
36
+ private readonly streamOpenTimeoutMs: number;
37
+ private readonly streamReconnectDelayMs: number;
38
+ private readonly streamReconnectMaxDelayMs: number;
39
+ private readonly listenKeyKeepAliveMs: number;
40
+ private readonly records = new Map<string, PrivateSubscriptionRecord>();
41
+
42
+ constructor(
43
+ context: ClientContext,
44
+ adapter: PrivateUserDataAdapter,
45
+ accountConsumer: PrivateAccountDataConsumer,
46
+ orderConsumer: PrivateOrderDataConsumer,
47
+ options: AccountRuntimeOptions = {},
48
+ ) {
49
+ this.context = context;
50
+ this.adapter = adapter;
51
+ this.accountConsumer = accountConsumer;
52
+ this.orderConsumer = orderConsumer;
53
+ this.streamOpenTimeoutMs =
54
+ options.streamOpenTimeoutMs ?? DEFAULT_STREAM_OPEN_TIMEOUT_MS;
55
+ this.streamReconnectDelayMs =
56
+ options.streamReconnectDelayMs ?? DEFAULT_STREAM_RECONNECT_DELAY_MS;
57
+ this.streamReconnectMaxDelayMs =
58
+ options.streamReconnectMaxDelayMs ??
59
+ DEFAULT_STREAM_RECONNECT_MAX_DELAY_MS;
60
+ this.listenKeyKeepAliveMs =
61
+ options.listenKeyKeepAliveMs ?? DEFAULT_LISTEN_KEY_KEEPALIVE_MS;
62
+ }
63
+
64
+ async subscribeAccountFeed(accountId: string): Promise<void> {
65
+ const account = this.getAccount(accountId);
66
+ const record = this.getOrCreateRecord(account);
67
+ const needsPending = !record.stream && !record.startPromise;
68
+ record.accountSubscribed = true;
69
+ if (needsPending) {
70
+ this.accountConsumer.onPrivateAccountPending(accountId, record.exchange);
71
+ }
72
+
73
+ try {
74
+ await this.ensureStream(record, account);
75
+ await this.bootstrapAccount(record, account);
76
+ } catch (error) {
77
+ record.accountSubscribed = false;
78
+ this.closeIfUnused(record);
79
+ throw error;
80
+ }
81
+ }
82
+
83
+ unsubscribeAccountFeed(accountId: string): void {
84
+ const record = this.records.get(accountId);
85
+ if (!record) {
86
+ return;
87
+ }
88
+
89
+ record.accountSubscribed = false;
90
+ this.closeIfUnused(record);
91
+ }
92
+
93
+ async subscribeOrderFeed(accountId: string): Promise<void> {
94
+ const account = this.getAccount(accountId);
95
+ const record = this.getOrCreateRecord(account);
96
+ const needsPending = !record.stream && !record.startPromise;
97
+ record.ordersSubscribed = true;
98
+ if (needsPending) {
99
+ this.orderConsumer.onPrivateOrderPending(accountId, record.exchange);
100
+ }
101
+
102
+ try {
103
+ await this.ensureStream(record, account);
104
+ await this.bootstrapOrders(record, account);
105
+ } catch (error) {
106
+ record.ordersSubscribed = false;
107
+ this.closeIfUnused(record);
108
+ throw error;
109
+ }
110
+ }
111
+
112
+ unsubscribeOrderFeed(accountId: string): void {
113
+ const record = this.records.get(accountId);
114
+ if (!record) {
115
+ return;
116
+ }
117
+
118
+ record.ordersSubscribed = false;
119
+ this.closeIfUnused(record);
120
+ }
121
+
122
+ onClientStarted(): void {
123
+ for (const record of this.records.values()) {
124
+ if (!this.isActive(record)) {
125
+ continue;
126
+ }
127
+
128
+ if (record.accountSubscribed) {
129
+ this.accountConsumer.onPrivateAccountPending(
130
+ record.accountId,
131
+ record.exchange,
132
+ );
133
+ }
134
+ if (record.ordersSubscribed) {
135
+ this.orderConsumer.onPrivateOrderPending(
136
+ record.accountId,
137
+ record.exchange,
138
+ );
139
+ }
140
+
141
+ void this.resumeRecord(record);
142
+ }
143
+ }
144
+
145
+ onClientStopping(): void {
146
+ for (const record of this.records.values()) {
147
+ this.closeStream(record);
148
+ }
149
+ }
150
+
151
+ onAccountRemoved(accountId: string): void {
152
+ const record = this.records.get(accountId);
153
+ if (!record) {
154
+ return;
155
+ }
156
+
157
+ this.closeStream(record);
158
+ this.records.delete(accountId);
159
+ }
160
+
161
+ onCredentialsUpdated(accountId: string): void {
162
+ const record = this.records.get(accountId);
163
+ if (!record || !this.isActive(record)) {
164
+ return;
165
+ }
166
+
167
+ if (record.accountSubscribed) {
168
+ this.accountConsumer.onPrivateAccountPending(accountId, record.exchange);
169
+ }
170
+ if (record.ordersSubscribed) {
171
+ this.orderConsumer.onPrivateOrderPending(accountId, record.exchange);
172
+ }
173
+
174
+ void this.resumeRecord(record);
175
+ }
176
+
177
+ private async resumeRecord(record: PrivateSubscriptionRecord): Promise<void> {
178
+ const account = this.getAccount(record.accountId);
179
+ this.closeStream(record);
180
+
181
+ try {
182
+ await this.ensureStream(record, account);
183
+ if (record.accountSubscribed) {
184
+ await this.bootstrapAccount(record, account);
185
+ }
186
+ if (record.ordersSubscribed) {
187
+ await this.bootstrapOrders(record, account);
188
+ }
189
+ } catch {
190
+ // Errors are already published to the runtime error bus.
191
+ }
192
+ }
193
+
194
+ private getAccount(accountId: string): RegisteredAccountRecord {
195
+ const account = this.context.getRegisteredAccount(accountId);
196
+ if (account.exchange !== this.adapter.exchange) {
197
+ throw new AcexError(
198
+ "EXCHANGE_NOT_SUPPORTED",
199
+ `Exchange is not supported yet: ${account.exchange}`,
200
+ );
201
+ }
202
+
203
+ return account;
204
+ }
205
+
206
+ private getOrCreateRecord(
207
+ account: RegisteredAccountRecord,
208
+ ): PrivateSubscriptionRecord {
209
+ const existing = this.records.get(account.accountId);
210
+ if (existing) {
211
+ return existing;
212
+ }
213
+
214
+ const record: PrivateSubscriptionRecord = {
215
+ accountId: account.accountId,
216
+ exchange: account.exchange,
217
+ accountSubscribed: false,
218
+ ordersSubscribed: false,
219
+ accountReady: false,
220
+ orderReady: false,
221
+ };
222
+
223
+ this.records.set(account.accountId, record);
224
+ return record;
225
+ }
226
+
227
+ private isActive(record: PrivateSubscriptionRecord): boolean {
228
+ return record.accountSubscribed || record.ordersSubscribed;
229
+ }
230
+
231
+ private closeIfUnused(record: PrivateSubscriptionRecord): void {
232
+ if (this.isActive(record)) {
233
+ return;
234
+ }
235
+
236
+ this.closeStream(record);
237
+ this.records.delete(record.accountId);
238
+ }
239
+
240
+ private closeStream(record: PrivateSubscriptionRecord): void {
241
+ record.stream?.close();
242
+ record.stream = undefined;
243
+ }
244
+
245
+ private async ensureStream(
246
+ record: PrivateSubscriptionRecord,
247
+ account: RegisteredAccountRecord,
248
+ ): Promise<void> {
249
+ if (record.stream) {
250
+ return;
251
+ }
252
+
253
+ if (!record.startPromise) {
254
+ record.startPromise = this.startStream(record, account);
255
+ }
256
+
257
+ try {
258
+ await record.startPromise;
259
+ } finally {
260
+ if (record.startPromise) {
261
+ record.startPromise = undefined;
262
+ }
263
+ }
264
+ }
265
+
266
+ private async startStream(
267
+ record: PrivateSubscriptionRecord,
268
+ account: RegisteredAccountRecord,
269
+ ): Promise<void> {
270
+ const credentials = account.credentials;
271
+ if (!credentials) {
272
+ throw new AcexError(
273
+ "CREDENTIALS_MISSING",
274
+ `Account credentials are required for private subscriptions: ${account.accountId}`,
275
+ );
276
+ }
277
+
278
+ const stream = this.adapter.createPrivateStream(
279
+ credentials,
280
+ {
281
+ onAccountUpdate: (update) => {
282
+ if (!record.accountSubscribed) {
283
+ return;
284
+ }
285
+
286
+ record.accountReady = true;
287
+ this.accountConsumer.onPrivateAccountUpdate(
288
+ record.accountId,
289
+ record.exchange,
290
+ update,
291
+ );
292
+ },
293
+ onOrderUpdate: (update) => {
294
+ if (!record.ordersSubscribed) {
295
+ return;
296
+ }
297
+
298
+ record.orderReady = true;
299
+ this.orderConsumer.onPrivateOrderUpdate(
300
+ record.accountId,
301
+ record.exchange,
302
+ update,
303
+ );
304
+ },
305
+ onDisconnected: () => {
306
+ if (record.accountSubscribed) {
307
+ this.accountConsumer.onPrivateAccountStreamState(
308
+ record.accountId,
309
+ record.exchange,
310
+ {
311
+ runtimeStatus: "reconnecting",
312
+ ready: record.accountReady,
313
+ reason: "ws_disconnected",
314
+ },
315
+ );
316
+ }
317
+ if (record.ordersSubscribed) {
318
+ this.orderConsumer.onPrivateOrderStreamState(
319
+ record.accountId,
320
+ record.exchange,
321
+ {
322
+ runtimeStatus: "reconnecting",
323
+ ready: record.orderReady,
324
+ reason: "ws_disconnected",
325
+ },
326
+ );
327
+ }
328
+ },
329
+ onReconnected: () => {
330
+ if (!record.reconcilePromise) {
331
+ record.reconcilePromise = this.reconcileRecord(record)
332
+ .catch(() => {})
333
+ .finally(() => {
334
+ record.reconcilePromise = undefined;
335
+ });
336
+ }
337
+ },
338
+ onError: (error) => {
339
+ this.context.publishRuntimeError("adapter", error, {
340
+ accountId: record.accountId,
341
+ exchange: record.exchange,
342
+ });
343
+ },
344
+ },
345
+ {
346
+ openTimeoutMs: this.streamOpenTimeoutMs,
347
+ reconnectDelayMs: this.streamReconnectDelayMs,
348
+ reconnectMaxDelayMs: this.streamReconnectMaxDelayMs,
349
+ listenKeyKeepAliveMs: this.listenKeyKeepAliveMs,
350
+ now: () => this.context.now(),
351
+ },
352
+ account.options,
353
+ );
354
+
355
+ record.stream = stream;
356
+
357
+ try {
358
+ await stream.ready;
359
+ } catch (error) {
360
+ this.closeStream(record);
361
+ const runtimeError =
362
+ error instanceof Error
363
+ ? error
364
+ : new Error("Failed to open Binance private stream");
365
+ this.context.publishRuntimeError("adapter", runtimeError, {
366
+ accountId: record.accountId,
367
+ exchange: record.exchange,
368
+ });
369
+
370
+ if (record.accountSubscribed) {
371
+ this.accountConsumer.onPrivateAccountStreamState(
372
+ record.accountId,
373
+ record.exchange,
374
+ {
375
+ runtimeStatus: "degraded",
376
+ ready: record.accountReady,
377
+ reason: "ws_disconnected",
378
+ },
379
+ );
380
+ }
381
+ if (record.ordersSubscribed) {
382
+ this.orderConsumer.onPrivateOrderStreamState(
383
+ record.accountId,
384
+ record.exchange,
385
+ {
386
+ runtimeStatus: "degraded",
387
+ ready: record.orderReady,
388
+ reason: "ws_disconnected",
389
+ },
390
+ );
391
+ }
392
+
393
+ throw runtimeError;
394
+ }
395
+ }
396
+
397
+ private async reconcileRecord(
398
+ record: PrivateSubscriptionRecord,
399
+ ): Promise<void> {
400
+ const account = this.getAccount(record.accountId);
401
+
402
+ if (record.accountSubscribed) {
403
+ this.accountConsumer.onPrivateAccountPending(
404
+ record.accountId,
405
+ record.exchange,
406
+ );
407
+ await this.bootstrapAccount(record, account);
408
+ }
409
+
410
+ if (record.ordersSubscribed) {
411
+ this.orderConsumer.onPrivateOrderPending(
412
+ record.accountId,
413
+ record.exchange,
414
+ );
415
+ await this.bootstrapOrders(record, account);
416
+ }
417
+ }
418
+
419
+ private async bootstrapAccount(
420
+ record: PrivateSubscriptionRecord,
421
+ account: RegisteredAccountRecord,
422
+ ): Promise<void> {
423
+ try {
424
+ const bootstrap = await this.adapter.bootstrapAccount(
425
+ account.credentials ?? {},
426
+ account.options,
427
+ );
428
+ if (!record.accountSubscribed) {
429
+ return;
430
+ }
431
+
432
+ record.accountReady = true;
433
+ this.accountConsumer.onPrivateAccountBootstrap(
434
+ record.accountId,
435
+ record.exchange,
436
+ bootstrap,
437
+ );
438
+ } catch (error) {
439
+ record.accountReady = false;
440
+ this.context.publishRuntimeError(
441
+ "adapter",
442
+ error instanceof Error
443
+ ? error
444
+ : new Error("Failed to bootstrap Binance private account state"),
445
+ {
446
+ accountId: record.accountId,
447
+ exchange: record.exchange,
448
+ },
449
+ );
450
+ this.accountConsumer.onPrivateAccountStreamState(
451
+ record.accountId,
452
+ record.exchange,
453
+ {
454
+ runtimeStatus: "degraded",
455
+ ready: false,
456
+ reason: "auth_failed",
457
+ },
458
+ );
459
+ throw new AcexError(
460
+ "ACCOUNT_BOOTSTRAP_FAILED",
461
+ `Failed to bootstrap account data: ${record.accountId}`,
462
+ );
463
+ }
464
+ }
465
+
466
+ private async bootstrapOrders(
467
+ record: PrivateSubscriptionRecord,
468
+ account: RegisteredAccountRecord,
469
+ ): Promise<void> {
470
+ try {
471
+ const snapshots = await this.adapter.bootstrapOpenOrders(
472
+ account.credentials ?? {},
473
+ account.options,
474
+ );
475
+ if (!record.ordersSubscribed) {
476
+ return;
477
+ }
478
+
479
+ record.orderReady = true;
480
+ this.orderConsumer.onPrivateOrderBootstrap(
481
+ record.accountId,
482
+ record.exchange,
483
+ snapshots,
484
+ );
485
+ } catch (error) {
486
+ record.orderReady = false;
487
+ this.context.publishRuntimeError(
488
+ "adapter",
489
+ error instanceof Error
490
+ ? error
491
+ : new Error("Failed to bootstrap Binance private order state"),
492
+ {
493
+ accountId: record.accountId,
494
+ exchange: record.exchange,
495
+ },
496
+ );
497
+ this.orderConsumer.onPrivateOrderStreamState(
498
+ record.accountId,
499
+ record.exchange,
500
+ {
501
+ runtimeStatus: "degraded",
502
+ ready: false,
503
+ reason: "auth_failed",
504
+ },
505
+ );
506
+ throw new AcexError(
507
+ "ORDER_BOOTSTRAP_FAILED",
508
+ `Failed to bootstrap order data: ${record.accountId}`,
509
+ );
510
+ }
511
+ }
512
+ }