@imbingox/acex 0.4.0-beta.16 → 0.4.0-beta.17

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @imbingox/acex
2
2
 
3
+ ## 0.4.0-beta.17
4
+
5
+ ### Minor Changes
6
+
7
+ - 35b8163: 事件流新增 `conflate` / `buffer` 与 `maxBuffer` 订阅选项:L1 Book 与 Funding Rate 默认改为 latest-wins,慢消费者只保留同一 `venue:symbol` 的最新事件;market status 事件按 activity/ready/freshness/reason 去重发布;buffer 溢出会丢弃最旧事件并通过 `EVENT_BUFFER_OVERFLOW` runtime error 告警。
8
+
3
9
  ## 0.4.0-beta.16
4
10
 
5
11
  ### Minor Changes
package/docs/api.md CHANGED
@@ -372,6 +372,26 @@ console.log(time.serverTime, time.roundTripMs, time.estimatedOffsetMs);
372
372
 
373
373
  Funding Rate 当前通过 Binance mark price websocket 更新,仅支持永续合约(`MarketDefinition.type === "swap"`,包括 Binance TradFi Perps)。spot 或 future 订阅会抛 `MARKET_FUNDING_RATE_UNSUPPORTED`。
374
374
 
375
+ ### 5.5 事件流 options
376
+
377
+ Market 事件流支持可选第二参:
378
+
379
+ ```ts
380
+ type EventStreamOptions = {
381
+ mode?: "conflate" | "buffer";
382
+ maxBuffer?: number;
383
+ };
384
+
385
+ client.market.events.l1BookUpdates(
386
+ { venue: "binance", symbol: "BTC/USDT:USDT" },
387
+ { mode: "buffer", maxBuffer: 50_000 },
388
+ );
389
+ ```
390
+
391
+ `l1BookUpdates()` 与 `fundingRateUpdates()` 默认使用 `conflate`,同一 `venue:symbol` 慢消费者只保留最新事件,适合策略热路径。需要录制每个 tick 时显式传 `{ mode: "buffer" }`。`market.events.all()` 与 `market.events.status()` 默认使用 `buffer`;显式传 `{ mode: "conflate" }` 时,`all()` 按 `type:venue:symbol` 合并,`status()` 按 `venue:symbol` 合并。
392
+
393
+ `buffer` 模式默认每个订阅者最多积压 `10_000` 条事件,超过后丢弃最旧事件。每次积压 episode 只会向 `client.events.errors()` 发布一次 `EVENT_BUFFER_OVERFLOW` runtime error,事件 metadata 包含 `stream` 与 `maxBuffer`;队列排空后再次溢出会再次告警。`conflate` 模式天然有界,不使用 `maxBuffer`。
394
+
375
395
  ## 6. AccountManager
376
396
 
377
397
  ```ts
@@ -400,7 +420,7 @@ Account 事件用于消费余额、仓位、风险或全量快照替换:
400
420
  ```ts
401
421
  for await (const event of client.account.events.updates({
402
422
  accountId: "main-binance",
403
- })) {
423
+ }, { maxBuffer: 20_000 })) {
404
424
  if (event.type === "risk.updated") {
405
425
  console.log(event.snapshot.riskRatio);
406
426
  }
@@ -408,6 +428,8 @@ for await (const event of client.account.events.updates({
408
428
  }
409
429
  ```
410
430
 
431
+ Account 事件流只支持 `{ maxBuffer?: number }`,不提供 conflate;余额、仓位、风险和状态事件默认按 buffer 语义保留顺序。
432
+
411
433
  ## 7. OrderManager
412
434
 
413
435
  ```ts
@@ -456,7 +478,7 @@ Order 事件用于消费订单状态变化和 open orders 快照校准。Binance
456
478
  for await (const event of client.order.events.updates({
457
479
  accountId: "main-binance",
458
480
  symbol: "BTC/USDT:USDT",
459
- })) {
481
+ }, { maxBuffer: 20_000 })) {
460
482
  if (event.type === "order.filled") {
461
483
  console.log(event.snapshot.filled);
462
484
  }
@@ -464,23 +486,28 @@ for await (const event of client.order.events.updates({
464
486
  }
465
487
  ```
466
488
 
489
+ Order 事件流只支持 `{ maxBuffer?: number }`,不提供 conflate;订单中间状态和错误恢复信号默认按 buffer 语义保留顺序。
490
+
467
491
  ## 8. 健康与错误事件
468
492
 
469
493
  ```ts
470
494
  const health = client.getHealth();
471
495
 
472
- for await (const event of client.events.health({ venue: "binance" })) {
496
+ for await (const event of client.events.health(
497
+ { venue: "binance" },
498
+ { maxBuffer: 20_000 },
499
+ )) {
473
500
  console.log(event.type);
474
501
  break;
475
502
  }
476
503
 
477
- for await (const error of client.events.errors()) {
504
+ for await (const error of client.events.errors({ maxBuffer: 20_000 })) {
478
505
  console.error(error.source, error.error);
479
506
  break;
480
507
  }
481
508
  ```
482
509
 
483
- `getHealth()` 聚合 client、market、account、order 的当前状态。`events.health(filter)` 只返回满足 filter 的事件;如果事件没有 filter 请求的字段,会被过滤掉。
510
+ `getHealth()` 聚合 client、market、account、order 的当前状态。`events.health(filter, options?)` 只返回满足 filter 的事件;如果事件没有 filter 请求的字段,会被过滤掉。`events.health()` 与 `events.errors()` 只支持 `{ maxBuffer?: number }`,默认 buffer 上限同样是 `10_000`;`errors()` 自身溢出时只丢弃最旧错误事件,不再发布新的 overflow 错误,避免递归。
484
511
 
485
512
  ## 9. 数据类型速查
486
513
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imbingox/acex",
3
- "version": "0.4.0-beta.16",
3
+ "version": "0.4.0-beta.17",
4
4
  "description": "Multi-exchange trading SDK for market data, account, and order management",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,6 +15,7 @@ import {
15
15
  buildAcexErrorDetails,
16
16
  type VenueErrorReason,
17
17
  } from "../errors.ts";
18
+ import type { AsyncEventBusOverflowInfo } from "../internal/async-event-bus.ts";
18
19
  import { AsyncEventBus } from "../internal/async-event-bus.ts";
19
20
  import { matchesHealthFilter } from "../internal/filters.ts";
20
21
  import { ReactiveRateLimiter } from "../internal/rate-limiter.ts";
@@ -26,6 +27,7 @@ import type {
26
27
  AccountManager,
27
28
  AcexClient,
28
29
  AcexInternalError,
30
+ BufferedEventStreamOptions,
29
31
  CancelAllOrdersInput,
30
32
  CancelOrderInput,
31
33
  ClientEventStreams,
@@ -75,14 +77,30 @@ class ClientEventStreamsImpl implements ClientEventStreams {
75
77
  constructor(
76
78
  private readonly healthBus: AsyncEventBus<HealthEvent>,
77
79
  private readonly errorBus: AsyncEventBus<AcexInternalError>,
80
+ private readonly onHealthOverflow: (
81
+ info: AsyncEventBusOverflowInfo,
82
+ ) => void,
78
83
  ) {}
79
84
 
80
- errors(): AsyncIterable<AcexInternalError> {
81
- return this.errorBus.stream();
85
+ errors(
86
+ options?: BufferedEventStreamOptions,
87
+ ): AsyncIterable<AcexInternalError> {
88
+ return this.errorBus.stream(() => true, {
89
+ maxBuffer: options?.maxBuffer,
90
+ });
82
91
  }
83
92
 
84
- health(filter?: HealthEventFilter): AsyncIterable<HealthEvent> {
85
- return this.healthBus.stream((event) => matchesHealthFilter(event, filter));
93
+ health(
94
+ filter?: HealthEventFilter,
95
+ options?: BufferedEventStreamOptions,
96
+ ): AsyncIterable<HealthEvent> {
97
+ return this.healthBus.stream(
98
+ (event) => matchesHealthFilter(event, filter),
99
+ {
100
+ maxBuffer: options?.maxBuffer,
101
+ onOverflow: this.onHealthOverflow,
102
+ },
103
+ );
86
104
  }
87
105
  }
88
106
 
@@ -149,7 +167,11 @@ export class AcexClientImpl implements AcexClient, ClientContext {
149
167
  this.market = this.marketManager;
150
168
  this.account = this.accountManager;
151
169
  this.order = this.orderManager;
152
- this.events = new ClientEventStreamsImpl(this.healthBus, this.errorBus);
170
+ this.events = new ClientEventStreamsImpl(
171
+ this.healthBus,
172
+ this.errorBus,
173
+ this.createOverflowHandler("client.health"),
174
+ );
153
175
  }
154
176
 
155
177
  // --- AcexClient public API ---
@@ -421,6 +443,21 @@ export class AcexClientImpl implements AcexClient, ClientContext {
421
443
  this.healthBus.publish(event);
422
444
  }
423
445
 
446
+ private createOverflowHandler(
447
+ stream: string,
448
+ ): (info: AsyncEventBusOverflowInfo) => void {
449
+ return ({ maxBuffer }) => {
450
+ const error = new AcexError(
451
+ "EVENT_BUFFER_OVERFLOW",
452
+ `Event stream buffer overflow: ${stream}`,
453
+ );
454
+ this.publishRuntimeError("runtime", error, {
455
+ stream,
456
+ maxBuffer,
457
+ });
458
+ };
459
+ }
460
+
424
461
  // --- Private ---
425
462
 
426
463
  private setClientStatus(status: ClientStatus): void {
package/src/errors.ts CHANGED
@@ -7,6 +7,7 @@ export type AcexErrorCode =
7
7
  | "ACCOUNT_NOT_FOUND"
8
8
  | "CLIENT_NOT_STARTED"
9
9
  | "CREDENTIALS_MISSING"
10
+ | "EVENT_BUFFER_OVERFLOW"
10
11
  | "VENUE_NOT_SUPPORTED"
11
12
  | "MARKET_CATALOG_LOAD_FAILED"
12
13
  | "MARKET_SERVER_TIME_FETCH_FAILED"
@@ -1,10 +1,25 @@
1
1
  type EventPredicate<T> = (event: T) => boolean;
2
2
 
3
+ export type AsyncEventBusStreamMode = "buffer" | "conflate";
4
+
5
+ export interface AsyncEventBusOverflowInfo {
6
+ maxBuffer: number;
7
+ }
8
+
9
+ export interface AsyncEventBusStreamOptions<T> {
10
+ mode?: AsyncEventBusStreamMode;
11
+ maxBuffer?: number;
12
+ conflateKey?: (event: T) => string;
13
+ onOverflow?: (info: AsyncEventBusOverflowInfo) => void;
14
+ }
15
+
3
16
  interface BusListener<T> {
4
17
  close(): void;
5
18
  dispatch(event: T): void;
6
19
  }
7
20
 
21
+ const DEFAULT_MAX_BUFFER = 10_000;
22
+
8
23
  function doneResult<T>(): IteratorResult<T> {
9
24
  return { done: true, value: undefined as T };
10
25
  }
@@ -20,11 +35,67 @@ export class AsyncEventBus<T> {
20
35
 
21
36
  stream<U extends T = T>(
22
37
  filter: ((event: T) => event is U) | EventPredicate<T> = () => true,
38
+ options: AsyncEventBusStreamOptions<U> = {},
23
39
  ): AsyncIterable<U> {
24
40
  let closed = false;
25
- const queue: U[] = [];
41
+ const mode = options.mode ?? "buffer";
42
+ const maxBuffer = options.maxBuffer ?? DEFAULT_MAX_BUFFER;
43
+ const bufferQueue: U[] = [];
44
+ const conflateQueue =
45
+ mode === "conflate" ? new Map<string, U>() : undefined;
46
+ let overflowNotified = false;
26
47
  let pendingResolve: ((result: IteratorResult<U>) => void) | undefined;
27
48
 
49
+ if (mode === "conflate" && !options.conflateKey) {
50
+ throw new Error("AsyncEventBus conflate mode requires conflateKey");
51
+ }
52
+
53
+ const resetOverflowIfDrained = () => {
54
+ if (bufferQueue.length === 0) {
55
+ overflowNotified = false;
56
+ }
57
+ };
58
+
59
+ const enqueue = (event: U) => {
60
+ if (conflateQueue) {
61
+ const key = options.conflateKey?.(event);
62
+ if (key === undefined) {
63
+ throw new Error("AsyncEventBus conflate mode requires conflateKey");
64
+ }
65
+ conflateQueue.set(key, event);
66
+ return;
67
+ }
68
+
69
+ bufferQueue.push(event);
70
+
71
+ if (bufferQueue.length <= maxBuffer) {
72
+ return;
73
+ }
74
+
75
+ bufferQueue.shift();
76
+ if (!overflowNotified) {
77
+ overflowNotified = true;
78
+ options.onOverflow?.({ maxBuffer });
79
+ }
80
+ };
81
+
82
+ const dequeue = (): U | undefined => {
83
+ if (conflateQueue) {
84
+ const first = conflateQueue.entries().next();
85
+ if (first.done) {
86
+ return undefined;
87
+ }
88
+
89
+ const [key, event] = first.value;
90
+ conflateQueue.delete(key);
91
+ return event;
92
+ }
93
+
94
+ const event = bufferQueue.shift();
95
+ resetOverflowIfDrained();
96
+ return event;
97
+ };
98
+
28
99
  const close = () => {
29
100
  if (closed) {
30
101
  return;
@@ -55,7 +126,7 @@ export class AsyncEventBus<T> {
55
126
  return;
56
127
  }
57
128
 
58
- queue.push(typedEvent);
129
+ enqueue(typedEvent);
59
130
  },
60
131
  };
61
132
 
@@ -70,11 +141,12 @@ export class AsyncEventBus<T> {
70
141
  return doneResult<U>();
71
142
  }
72
143
 
73
- const queued = queue.shift();
144
+ const queued = dequeue();
74
145
  if (queued !== undefined) {
75
146
  return { done: false, value: queued };
76
147
  }
77
148
 
149
+ resetOverflowIfDrained();
78
150
  return await new Promise<IteratorResult<U>>((resolve) => {
79
151
  pendingResolve = resolve;
80
152
  });
@@ -14,6 +14,8 @@ import type {
14
14
  PrivateAccountDataConsumer,
15
15
  PrivateSubscriptionState,
16
16
  } from "../client/context.ts";
17
+ import { AcexError } from "../errors.ts";
18
+ import type { AsyncEventBusOverflowInfo } from "../internal/async-event-bus.ts";
17
19
  import { AsyncEventBus } from "../internal/async-event-bus.ts";
18
20
  import { toCanonical } from "../internal/decimal.ts";
19
21
  import { matchesAccountFilter } from "../internal/filters.ts";
@@ -125,23 +127,33 @@ export class AccountManagerImpl
125
127
  this.context = context;
126
128
 
127
129
  this.events = {
128
- status: (filter) =>
129
- this.accountStatusBus.stream((event) =>
130
- matchesAccountFilter(
131
- { accountId: event.accountId, venue: event.venue },
132
- filter,
133
- ),
130
+ status: (filter, options) =>
131
+ this.accountStatusBus.stream(
132
+ (event) =>
133
+ matchesAccountFilter(
134
+ { accountId: event.accountId, venue: event.venue },
135
+ filter,
136
+ ),
137
+ {
138
+ maxBuffer: options?.maxBuffer,
139
+ onOverflow: this.createOverflowHandler("account.status"),
140
+ },
134
141
  ),
135
- updates: (filter) =>
136
- this.accountBus.stream((event) =>
137
- matchesAccountFilter(
138
- {
139
- accountId: event.accountId,
140
- venue: event.venue,
141
- symbol: "symbol" in event ? event.symbol : undefined,
142
- },
143
- filter,
144
- ),
142
+ updates: (filter, options) =>
143
+ this.accountBus.stream(
144
+ (event) =>
145
+ matchesAccountFilter(
146
+ {
147
+ accountId: event.accountId,
148
+ venue: event.venue,
149
+ symbol: "symbol" in event ? event.symbol : undefined,
150
+ },
151
+ filter,
152
+ ),
153
+ {
154
+ maxBuffer: options?.maxBuffer,
155
+ onOverflow: this.createOverflowHandler("account.updates"),
156
+ },
145
157
  ),
146
158
  };
147
159
  }
@@ -872,4 +884,19 @@ export class AccountManagerImpl
872
884
  this.accountStatusBus.publish(event);
873
885
  this.context.publishHealthEvent(event);
874
886
  }
887
+
888
+ private createOverflowHandler(
889
+ stream: string,
890
+ ): (info: AsyncEventBusOverflowInfo) => void {
891
+ return ({ maxBuffer }) => {
892
+ const error = new AcexError(
893
+ "EVENT_BUFFER_OVERFLOW",
894
+ `Event stream buffer overflow: ${stream}`,
895
+ );
896
+ this.context.publishRuntimeError("account", error, {
897
+ stream,
898
+ maxBuffer,
899
+ });
900
+ };
901
+ }
875
902
  }
@@ -19,10 +19,15 @@ import {
19
19
  buildAcexErrorDetails,
20
20
  formatAcexErrorMessage,
21
21
  } from "../errors.ts";
22
+ import type {
23
+ AsyncEventBusOverflowInfo,
24
+ AsyncEventBusStreamOptions,
25
+ } from "../internal/async-event-bus.ts";
22
26
  import { AsyncEventBus } from "../internal/async-event-bus.ts";
23
27
  import { toCanonical } from "../internal/decimal.ts";
24
28
  import { matchesMarketFilter } from "../internal/filters.ts";
25
29
  import type {
30
+ EventStreamOptions,
26
31
  FundingRateSnapshot,
27
32
  FundingRateUpdatedEvent,
28
33
  L1Book,
@@ -67,6 +72,14 @@ interface MarketRecord {
67
72
  status: MarketDataStatus;
68
73
  l1BookStream?: StreamHandle;
69
74
  fundingRateStream?: StreamHandle;
75
+ lastPublishedStatusKey?: MarketStatusPublicationKey;
76
+ }
77
+
78
+ interface MarketStatusPublicationKey {
79
+ activity: MarketDataStatus["activity"];
80
+ ready: MarketDataStatus["ready"];
81
+ freshness: MarketDataStatus["freshness"];
82
+ reason: MarketDataStatus["reason"];
70
83
  }
71
84
 
72
85
  interface CatalogFetchResult {
@@ -85,10 +98,38 @@ function marketKey(input: MarketKeyInput): string {
85
98
  return `${input.venue}:${input.symbol}`;
86
99
  }
87
100
 
101
+ function marketEventConflateKey(event: MarketEvent): string {
102
+ return `${event.type}:${marketKey(event)}`;
103
+ }
104
+
88
105
  function cloneMarketStatus(status: MarketDataStatus): MarketDataStatus {
89
106
  return { ...status };
90
107
  }
91
108
 
109
+ function statusPublicationKey(
110
+ status: MarketDataStatus,
111
+ ): MarketStatusPublicationKey {
112
+ return {
113
+ activity: status.activity,
114
+ ready: status.ready,
115
+ freshness: status.freshness,
116
+ reason: status.reason,
117
+ };
118
+ }
119
+
120
+ function sameStatusPublicationKey(
121
+ current: MarketStatusPublicationKey,
122
+ previous: MarketStatusPublicationKey | undefined,
123
+ ): boolean {
124
+ return (
125
+ previous !== undefined &&
126
+ current.activity === previous.activity &&
127
+ current.ready === previous.ready &&
128
+ current.freshness === previous.freshness &&
129
+ current.reason === previous.reason
130
+ );
131
+ }
132
+
92
133
  function cloneStreamStatus(
93
134
  status: MarketDataStreamStatus,
94
135
  ): MarketDataStreamStatus {
@@ -153,23 +194,49 @@ export class MarketManagerImpl
153
194
  options.l1ReconnectMaxDelayMs ?? DEFAULT_L1_RECONNECT_MAX_DELAY_MS;
154
195
 
155
196
  this.events = {
156
- all: (filter) =>
157
- this.marketBus.stream((event) => matchesMarketFilter(event, filter)),
158
- fundingRateUpdates: (filter) =>
197
+ all: (filter, options) =>
198
+ this.marketBus.stream(
199
+ (event) => matchesMarketFilter(event, filter),
200
+ this.createStreamOptions(
201
+ "market.all",
202
+ options,
203
+ "buffer",
204
+ marketEventConflateKey,
205
+ ),
206
+ ),
207
+ fundingRateUpdates: (filter, options) =>
159
208
  this.marketBus.stream(
160
209
  (event): event is FundingRateUpdatedEvent =>
161
210
  event.type === "funding_rate.updated" &&
162
211
  matchesMarketFilter(event, filter),
212
+ this.createStreamOptions(
213
+ "market.fundingRateUpdates",
214
+ options,
215
+ "conflate",
216
+ marketKey,
217
+ ),
163
218
  ),
164
- l1BookUpdates: (filter) =>
219
+ l1BookUpdates: (filter, options) =>
165
220
  this.marketBus.stream(
166
221
  (event): event is L1BookUpdatedEvent =>
167
222
  event.type === "l1_book.updated" &&
168
223
  matchesMarketFilter(event, filter),
224
+ this.createStreamOptions(
225
+ "market.l1BookUpdates",
226
+ options,
227
+ "conflate",
228
+ marketKey,
229
+ ),
169
230
  ),
170
- status: (filter) =>
171
- this.marketStatusBus.stream((event) =>
172
- matchesMarketFilter(event, filter),
231
+ status: (filter, options) =>
232
+ this.marketStatusBus.stream(
233
+ (event) => matchesMarketFilter(event, filter),
234
+ this.createStreamOptions(
235
+ "market.status",
236
+ options,
237
+ "buffer",
238
+ marketKey,
239
+ ),
173
240
  ),
174
241
  };
175
242
  }
@@ -1064,6 +1131,14 @@ export class MarketManagerImpl
1064
1131
  record.status.lastReadyAt = this.resolveLastReadyAt(record);
1065
1132
  }
1066
1133
 
1134
+ const publicationKey = statusPublicationKey(record.status);
1135
+ if (
1136
+ sameStatusPublicationKey(publicationKey, record.lastPublishedStatusKey)
1137
+ ) {
1138
+ return;
1139
+ }
1140
+
1141
+ record.lastPublishedStatusKey = publicationKey;
1067
1142
  this.publishStatus(record);
1068
1143
  }
1069
1144
 
@@ -1184,6 +1259,35 @@ export class MarketManagerImpl
1184
1259
  this.context.publishHealthEvent(event);
1185
1260
  }
1186
1261
 
1262
+ private createStreamOptions<U extends { venue: Venue; symbol: string }>(
1263
+ stream: string,
1264
+ options: EventStreamOptions | undefined,
1265
+ defaultMode: "buffer" | "conflate",
1266
+ conflateKey: (event: U) => string,
1267
+ ): AsyncEventBusStreamOptions<U> {
1268
+ return {
1269
+ mode: options?.mode ?? defaultMode,
1270
+ maxBuffer: options?.maxBuffer,
1271
+ conflateKey,
1272
+ onOverflow: this.createOverflowHandler(stream),
1273
+ };
1274
+ }
1275
+
1276
+ private createOverflowHandler(
1277
+ stream: string,
1278
+ ): (info: AsyncEventBusOverflowInfo) => void {
1279
+ return ({ maxBuffer }) => {
1280
+ const error = new AcexError(
1281
+ "EVENT_BUFFER_OVERFLOW",
1282
+ `Event stream buffer overflow: ${stream}`,
1283
+ );
1284
+ this.context.publishRuntimeError("market", error, {
1285
+ stream,
1286
+ maxBuffer,
1287
+ });
1288
+ };
1289
+ }
1290
+
1187
1291
  private async resumeStreams(): Promise<void> {
1188
1292
  for (const record of this.records.values()) {
1189
1293
  const market = record.market;
@@ -17,6 +17,7 @@ import {
17
17
  buildAcexErrorDetails,
18
18
  formatAcexErrorMessage,
19
19
  } from "../errors.ts";
20
+ import type { AsyncEventBusOverflowInfo } from "../internal/async-event-bus.ts";
20
21
  import { AsyncEventBus } from "../internal/async-event-bus.ts";
21
22
  import { matchesOrderFilter } from "../internal/filters.ts";
22
23
  import { isTransportError } from "../internal/http-client.ts";
@@ -116,23 +117,33 @@ export class OrderManagerImpl
116
117
  );
117
118
 
118
119
  this.events = {
119
- status: (filter) =>
120
- this.orderStatusBus.stream((event) =>
121
- matchesOrderFilter(
122
- { accountId: event.accountId, venue: event.venue },
123
- filter,
124
- ),
120
+ status: (filter, options) =>
121
+ this.orderStatusBus.stream(
122
+ (event) =>
123
+ matchesOrderFilter(
124
+ { accountId: event.accountId, venue: event.venue },
125
+ filter,
126
+ ),
127
+ {
128
+ maxBuffer: options?.maxBuffer,
129
+ onOverflow: this.createOverflowHandler("order.status"),
130
+ },
125
131
  ),
126
- updates: (filter) =>
127
- this.orderBus.stream((event) =>
128
- matchesOrderFilter(
129
- {
130
- accountId: event.accountId,
131
- venue: event.venue,
132
- symbol: "symbol" in event ? event.symbol : undefined,
133
- },
134
- filter,
135
- ),
132
+ updates: (filter, options) =>
133
+ this.orderBus.stream(
134
+ (event) =>
135
+ matchesOrderFilter(
136
+ {
137
+ accountId: event.accountId,
138
+ venue: event.venue,
139
+ symbol: "symbol" in event ? event.symbol : undefined,
140
+ },
141
+ filter,
142
+ ),
143
+ {
144
+ maxBuffer: options?.maxBuffer,
145
+ onOverflow: this.createOverflowHandler("order.updates"),
146
+ },
136
147
  ),
137
148
  };
138
149
  }
@@ -1270,6 +1281,21 @@ export class OrderManagerImpl
1270
1281
  },
1271
1282
  };
1272
1283
  }
1284
+
1285
+ private createOverflowHandler(
1286
+ stream: string,
1287
+ ): (info: AsyncEventBusOverflowInfo) => void {
1288
+ return ({ maxBuffer }) => {
1289
+ const error = new AcexError(
1290
+ "EVENT_BUFFER_OVERFLOW",
1291
+ `Event stream buffer overflow: ${stream}`,
1292
+ );
1293
+ this.context.publishRuntimeError("order", error, {
1294
+ stream,
1295
+ maxBuffer,
1296
+ });
1297
+ };
1298
+ }
1273
1299
  }
1274
1300
 
1275
1301
  function isOrderErrorCode(
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ BufferedEventStreamOptions,
2
3
  PrivateRuntimeReason,
3
4
  PrivateRuntimeStatus,
4
5
  SubscriptionActivity,
@@ -158,8 +159,14 @@ export type AccountEvent =
158
159
  | AccountSnapshotReplacedEvent;
159
160
 
160
161
  export interface AccountEventStreams {
161
- updates(filter?: AccountEventFilter): AsyncIterable<AccountEvent>;
162
- status(filter?: AccountEventFilter): AsyncIterable<AccountStatusChangedEvent>;
162
+ updates(
163
+ filter?: AccountEventFilter,
164
+ options?: BufferedEventStreamOptions,
165
+ ): AsyncIterable<AccountEvent>;
166
+ status(
167
+ filter?: AccountEventFilter,
168
+ options?: BufferedEventStreamOptions,
169
+ ): AsyncIterable<AccountStatusChangedEvent>;
163
170
  }
164
171
 
165
172
  export interface AccountManager {
@@ -18,6 +18,7 @@ import type {
18
18
  import type {
19
19
  AccountCredentials,
20
20
  AcexInternalError,
21
+ BufferedEventStreamOptions,
21
22
  ClientStatus,
22
23
  CreateClientOptions,
23
24
  RegisterAccountInput,
@@ -54,8 +55,13 @@ export interface HealthEventFilter {
54
55
  }
55
56
 
56
57
  export interface ClientEventStreams {
57
- health(filter?: HealthEventFilter): AsyncIterable<HealthEvent>;
58
- errors(): AsyncIterable<AcexInternalError>;
58
+ health(
59
+ filter?: HealthEventFilter,
60
+ options?: BufferedEventStreamOptions,
61
+ ): AsyncIterable<HealthEvent>;
62
+ errors(
63
+ options?: BufferedEventStreamOptions,
64
+ ): AsyncIterable<AcexInternalError>;
59
65
  }
60
66
 
61
67
  export type VenueRuntimeStatus = "available" | "type_only" | "reserved";
@@ -1,6 +1,11 @@
1
1
  import type BigNumber from "bignumber.js";
2
2
  import type { AcexError } from "../errors.ts";
3
- import type { MarketFreshness, SubscriptionActivity, Venue } from "./shared.ts";
3
+ import type {
4
+ EventStreamOptions,
5
+ MarketFreshness,
6
+ SubscriptionActivity,
7
+ Venue,
8
+ } from "./shared.ts";
4
9
 
5
10
  export type MarketType = "spot" | "swap" | "future";
6
11
 
@@ -170,12 +175,22 @@ export type MarketEvent =
170
175
  | MarketStatusChangedEvent;
171
176
 
172
177
  export interface MarketEventStreams {
173
- l1BookUpdates(filter?: MarketEventFilter): AsyncIterable<L1BookUpdatedEvent>;
178
+ l1BookUpdates(
179
+ filter?: MarketEventFilter,
180
+ options?: EventStreamOptions,
181
+ ): AsyncIterable<L1BookUpdatedEvent>;
174
182
  fundingRateUpdates(
175
183
  filter?: MarketEventFilter,
184
+ options?: EventStreamOptions,
176
185
  ): AsyncIterable<FundingRateUpdatedEvent>;
177
- status(filter?: MarketEventFilter): AsyncIterable<MarketStatusChangedEvent>;
178
- all(filter?: MarketEventFilter): AsyncIterable<MarketEvent>;
186
+ status(
187
+ filter?: MarketEventFilter,
188
+ options?: EventStreamOptions,
189
+ ): AsyncIterable<MarketStatusChangedEvent>;
190
+ all(
191
+ filter?: MarketEventFilter,
192
+ options?: EventStreamOptions,
193
+ ): AsyncIterable<MarketEvent>;
179
194
  }
180
195
 
181
196
  export interface MarketManager {
@@ -1,5 +1,6 @@
1
1
  import type { PositionSide } from "./account.ts";
2
2
  import type {
3
+ BufferedEventStreamOptions,
3
4
  PrivateRuntimeReason,
4
5
  PrivateRuntimeStatus,
5
6
  SubscriptionActivity,
@@ -160,8 +161,14 @@ export type OrderEvent =
160
161
  | OrderSnapshotReplacedEvent;
161
162
 
162
163
  export interface OrderEventStreams {
163
- updates(filter?: OrderEventFilter): AsyncIterable<OrderEvent>;
164
- status(filter?: OrderEventFilter): AsyncIterable<OrderStatusChangedEvent>;
164
+ updates(
165
+ filter?: OrderEventFilter,
166
+ options?: BufferedEventStreamOptions,
167
+ ): AsyncIterable<OrderEvent>;
168
+ status(
169
+ filter?: OrderEventFilter,
170
+ options?: BufferedEventStreamOptions,
171
+ ): AsyncIterable<OrderStatusChangedEvent>;
165
172
  }
166
173
 
167
174
  export interface OrderManager {
@@ -175,11 +175,24 @@ export interface StopOptions {
175
175
  timeoutMs?: number;
176
176
  }
177
177
 
178
+ export type EventStreamMode = "buffer" | "conflate";
179
+
180
+ export interface EventStreamOptions {
181
+ mode?: EventStreamMode;
182
+ maxBuffer?: number;
183
+ }
184
+
185
+ export interface BufferedEventStreamOptions {
186
+ maxBuffer?: number;
187
+ }
188
+
178
189
  export interface AcexInternalError {
179
190
  source: "client" | "market" | "account" | "order" | "adapter" | "runtime";
180
191
  venue?: Venue;
181
192
  accountId?: string;
182
193
  symbol?: string;
194
+ stream?: string;
195
+ maxBuffer?: number;
183
196
  error: Error;
184
197
  ts: number;
185
198
  }