@solard/sdk 0.2.31 → 0.2.37
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 +33 -0
- package/package.json +2 -2
- package/src/client.exact-sell.test.ts +95 -0
- package/src/client.listen-trades.test.ts +64 -0
- package/src/client.trade-fees.test.ts +37 -15
- package/src/client.ts +130 -47
- package/src/default.ts +1 -7
- package/src/export-surface.test.ts +41 -20
- package/src/index.ts +43 -40
- package/src/live.test.ts +243 -96
- package/src/live.ts +393 -191
- package/src/operator-api.architecture.test.ts +3 -1
package/src/live.ts
CHANGED
|
@@ -4,42 +4,61 @@ import {
|
|
|
4
4
|
subscribeMigrations as subscribeCoreMigrations,
|
|
5
5
|
subscribeTrades as subscribeCoreTrades,
|
|
6
6
|
type TradeEvent,
|
|
7
|
-
type TradeSubscription,
|
|
8
|
-
type MigrationEvent,
|
|
7
|
+
type TradeSubscription,
|
|
8
|
+
type MigrationEvent,
|
|
9
|
+
createLiveTradeHistoryRecorder,
|
|
10
|
+
type LiveTradeHistoryOptions,
|
|
9
11
|
} from "@solard/core";
|
|
10
12
|
|
|
11
|
-
export type LiveEndpointOptions = {
|
|
12
|
-
/** Ordered endpoints on the same cluster; public RPC can be supplied last. */
|
|
13
|
-
rpcUrls?: readonly string[];
|
|
14
|
-
/** Fresh HTTP and WebSocket readiness probes for pooled trade streams (default 30s). */
|
|
15
|
-
healthCheckIntervalMs?: number;
|
|
16
|
-
};
|
|
17
|
-
export type SubscribeLaunchesOptions = Omit<
|
|
13
|
+
export type LiveEndpointOptions = {
|
|
14
|
+
/** Ordered endpoints on the same cluster; public RPC can be supplied last. */
|
|
15
|
+
rpcUrls?: readonly string[];
|
|
16
|
+
/** Fresh HTTP and WebSocket readiness probes for pooled trade streams (default 30s). */
|
|
17
|
+
healthCheckIntervalMs?: number;
|
|
18
|
+
};
|
|
19
|
+
export type SubscribeLaunchesOptions = Omit<
|
|
18
20
|
Parameters<typeof subscribeCoreLaunches>[0],
|
|
19
21
|
"connection"
|
|
20
|
-
> &
|
|
22
|
+
> &
|
|
23
|
+
LiveEndpointOptions;
|
|
21
24
|
export type SubscribeMigrationsOptions = Omit<
|
|
22
25
|
Parameters<typeof subscribeCoreMigrations>[0],
|
|
23
26
|
"connection"
|
|
24
|
-
> &
|
|
27
|
+
> &
|
|
28
|
+
LiveEndpointOptions;
|
|
25
29
|
export type SubscribeTradesOptions = Omit<
|
|
26
30
|
Parameters<typeof subscribeCoreTrades>[0],
|
|
27
31
|
"connection"
|
|
28
|
-
> &
|
|
32
|
+
> &
|
|
33
|
+
LiveEndpointOptions;
|
|
29
34
|
|
|
30
|
-
export type ListenTradesOptions = Omit<SubscribeTradesOptions, "onTrade"
|
|
35
|
+
export type ListenTradesOptions = Omit<SubscribeTradesOptions, "onTrade"> & {
|
|
36
|
+
dbPath?: string;
|
|
37
|
+
/** Enabled by default; collected live history is partial and retains commitment. */
|
|
38
|
+
history?:
|
|
39
|
+
| false
|
|
40
|
+
| Pick<
|
|
41
|
+
LiveTradeHistoryOptions,
|
|
42
|
+
"flushIntervalMs" | "batchSize" | "maxPending"
|
|
43
|
+
>;
|
|
44
|
+
};
|
|
31
45
|
|
|
32
|
-
export type TradeListener = {
|
|
46
|
+
export type TradeListener = {
|
|
33
47
|
add(tokens: string | readonly string[]): Promise<void>;
|
|
34
48
|
remove(tokens: string | readonly string[]): Promise<void>;
|
|
35
49
|
has(token: string): boolean;
|
|
36
50
|
list(): string[];
|
|
37
|
-
onTrade(callback: (event: TradeEvent) => void | Promise<void>): () => void;
|
|
38
|
-
onMigration(
|
|
51
|
+
onTrade(callback: (event: TradeEvent) => void | Promise<void>): () => void;
|
|
52
|
+
onMigration(
|
|
53
|
+
callback: (event: TradeListenerMigration) => void | Promise<void>,
|
|
54
|
+
): () => void;
|
|
39
55
|
close(): Promise<void>;
|
|
40
56
|
readonly closed: Promise<void>;
|
|
41
|
-
};
|
|
42
|
-
export type TradeListenerMigration = MigrationEvent & {
|
|
57
|
+
};
|
|
58
|
+
export type TradeListenerMigration = MigrationEvent & {
|
|
59
|
+
oldPool: string | null;
|
|
60
|
+
newPool: string;
|
|
61
|
+
};
|
|
43
62
|
|
|
44
63
|
type LiveConnection = {
|
|
45
64
|
endpoint: string;
|
|
@@ -48,46 +67,76 @@ type LiveConnection = {
|
|
|
48
67
|
};
|
|
49
68
|
|
|
50
69
|
let shared: LiveConnection | null = null;
|
|
51
|
-
let readiness: { endpoint: string; promise: Promise<void> } | null = null;
|
|
52
|
-
|
|
53
|
-
/** HTTP + a fresh WebSocket handshake must both succeed before choosing an endpoint. */
|
|
54
|
-
export class LiveEndpointPool {
|
|
55
|
-
private active = 0;
|
|
56
|
-
readonly endpoints: readonly string[];
|
|
57
|
-
constructor(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
for (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
70
|
+
let readiness: { endpoint: string; promise: Promise<void> } | null = null;
|
|
71
|
+
|
|
72
|
+
/** HTTP + a fresh WebSocket handshake must both succeed before choosing an endpoint. */
|
|
73
|
+
export class LiveEndpointPool {
|
|
74
|
+
private active = 0;
|
|
75
|
+
readonly endpoints: readonly string[];
|
|
76
|
+
constructor(
|
|
77
|
+
endpoints: readonly string[],
|
|
78
|
+
private readonly probe: (endpoint: string) => Promise<void> = async (
|
|
79
|
+
endpoint,
|
|
80
|
+
) => {
|
|
81
|
+
await Promise.all([preflightRpc(endpoint), preflightWebSocket(endpoint)]);
|
|
82
|
+
},
|
|
83
|
+
) {
|
|
84
|
+
this.endpoints = [...new Set(endpoints)];
|
|
85
|
+
if (!this.endpoints.length)
|
|
86
|
+
throw new Error("Live RPC endpoint pool cannot be empty");
|
|
87
|
+
for (const endpoint of this.endpoints) deriveWebSocketEndpoint(endpoint);
|
|
88
|
+
}
|
|
89
|
+
get endpoint(): string {
|
|
90
|
+
return this.endpoints[this.active]!;
|
|
91
|
+
}
|
|
92
|
+
async select(skipCurrent = false): Promise<string> {
|
|
93
|
+
const start = this.active;
|
|
94
|
+
let lastError: unknown;
|
|
95
|
+
for (
|
|
96
|
+
let offset = skipCurrent ? 1 : 0;
|
|
97
|
+
offset < this.endpoints.length + (skipCurrent ? 1 : 0);
|
|
98
|
+
offset++
|
|
99
|
+
) {
|
|
100
|
+
const index = (start + offset) % this.endpoints.length;
|
|
101
|
+
try {
|
|
102
|
+
await this.probe(this.endpoints[index]!);
|
|
103
|
+
this.active = index;
|
|
104
|
+
return this.endpoint;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
lastError = error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
throw lastError;
|
|
110
|
+
}
|
|
111
|
+
async check(): Promise<string> {
|
|
112
|
+
try {
|
|
113
|
+
await this.probe(this.endpoint);
|
|
114
|
+
return this.endpoint;
|
|
115
|
+
} catch {
|
|
116
|
+
return await this.select(true);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const livePools = new Map<string, LiveEndpointPool>();
|
|
121
|
+
function endpointPool(
|
|
122
|
+
options: LiveEndpointOptions,
|
|
123
|
+
): LiveEndpointPool | undefined {
|
|
124
|
+
if (!options.rpcUrls?.length) return undefined;
|
|
125
|
+
const key = JSON.stringify(options.rpcUrls);
|
|
126
|
+
let pool = livePools.get(key);
|
|
127
|
+
if (!pool) {
|
|
128
|
+
pool = new LiveEndpointPool(options.rpcUrls);
|
|
129
|
+
livePools.set(key, pool);
|
|
130
|
+
}
|
|
131
|
+
return pool;
|
|
132
|
+
}
|
|
133
|
+
function connectionFor(endpoint: string): LiveConnection {
|
|
134
|
+
return {
|
|
135
|
+
endpoint,
|
|
136
|
+
websocketEndpoint: deriveWebSocketEndpoint(endpoint),
|
|
137
|
+
connection: new Connection(endpoint, "confirmed"),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
91
140
|
|
|
92
141
|
function endpointFromEnv(): string {
|
|
93
142
|
const endpoint = process.env.RPC_ENDPOINT?.trim();
|
|
@@ -289,9 +338,11 @@ async function ensureReady(endpoint: string): Promise<void> {
|
|
|
289
338
|
}
|
|
290
339
|
}
|
|
291
340
|
|
|
292
|
-
async function liveConnection(
|
|
293
|
-
|
|
294
|
-
|
|
341
|
+
async function liveConnection(
|
|
342
|
+
options: LiveEndpointOptions = {},
|
|
343
|
+
): Promise<LiveConnection> {
|
|
344
|
+
const pool = endpointPool(options);
|
|
345
|
+
if (pool) return connectionFor(await pool.select());
|
|
295
346
|
const endpoint = endpointFromEnv();
|
|
296
347
|
await ensureReady(endpoint);
|
|
297
348
|
if (shared?.endpoint === endpoint) return shared;
|
|
@@ -317,78 +368,120 @@ function notifyReady(
|
|
|
317
368
|
} catch {}
|
|
318
369
|
}
|
|
319
370
|
|
|
320
|
-
export async function subscribeTrades(options: SubscribeTradesOptions) {
|
|
321
|
-
let live = await liveConnection(options);
|
|
322
|
-
notifyReady(options, live);
|
|
323
|
-
const pool = endpointPool(options);
|
|
324
|
-
if (!pool)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
371
|
+
export async function subscribeTrades(options: SubscribeTradesOptions) {
|
|
372
|
+
let live = await liveConnection(options);
|
|
373
|
+
notifyReady(options, live);
|
|
374
|
+
const pool = endpointPool(options);
|
|
375
|
+
if (!pool)
|
|
376
|
+
return await subscribeCoreTrades({
|
|
377
|
+
...options,
|
|
378
|
+
connection: live.connection,
|
|
379
|
+
});
|
|
380
|
+
// Keep duplicate suppression across connection changes. Provider probes detect
|
|
381
|
+
// dead credentials/quota; they cannot diagnose a silent old socket when a new
|
|
382
|
+
// handshake to that provider remains healthy.
|
|
383
|
+
const seen = new Set<string>();
|
|
384
|
+
const onTrade = async (event: TradeEvent) => {
|
|
385
|
+
const key = [
|
|
386
|
+
event.signature,
|
|
387
|
+
event.venue,
|
|
388
|
+
event.mint,
|
|
389
|
+
event.pool,
|
|
390
|
+
event.eventIndex ?? `${event.side}:${event.baseRaw}:${event.quoteRaw}`,
|
|
391
|
+
].join(":");
|
|
392
|
+
if (seen.has(key)) return;
|
|
393
|
+
seen.add(key);
|
|
394
|
+
if (seen.size > 4096) seen.delete(seen.values().next().value!);
|
|
395
|
+
await options.onTrade(event);
|
|
396
|
+
};
|
|
397
|
+
let subscription = await subscribeCoreTrades({
|
|
398
|
+
...options,
|
|
399
|
+
signal: undefined,
|
|
400
|
+
onTrade,
|
|
401
|
+
connection: live.connection,
|
|
402
|
+
});
|
|
403
|
+
let stopped = false;
|
|
404
|
+
let tail: Promise<unknown> = Promise.resolve();
|
|
405
|
+
const serial = <T>(work: () => Promise<T>): Promise<T> => {
|
|
406
|
+
const pending = tail.then(work, work);
|
|
407
|
+
tail = pending.catch(() => undefined);
|
|
408
|
+
return pending;
|
|
409
|
+
};
|
|
410
|
+
let settle!: () => void;
|
|
411
|
+
const closed = new Promise<void>((resolve) => {
|
|
412
|
+
settle = resolve;
|
|
413
|
+
});
|
|
414
|
+
const requestedInterval = options.healthCheckIntervalMs ?? 30_000;
|
|
415
|
+
if (!Number.isFinite(requestedInterval) || requestedInterval < 1000) {
|
|
416
|
+
await subscription.close();
|
|
417
|
+
throw new Error("healthCheckIntervalMs must be at least 1000");
|
|
418
|
+
}
|
|
419
|
+
let checking = false;
|
|
420
|
+
const timer = setInterval(() => {
|
|
421
|
+
if (stopped || checking) return;
|
|
422
|
+
checking = true;
|
|
423
|
+
void serial(async () => {
|
|
424
|
+
if (stopped) return;
|
|
425
|
+
try {
|
|
426
|
+
const endpoint = await pool.check();
|
|
427
|
+
if (stopped || endpoint === live.endpoint) return;
|
|
428
|
+
const tokens = subscription.listTokens();
|
|
429
|
+
const nextLive = connectionFor(endpoint);
|
|
430
|
+
const next = await subscribeCoreTrades({
|
|
431
|
+
...options,
|
|
432
|
+
signal: undefined,
|
|
433
|
+
tokens,
|
|
434
|
+
onTrade,
|
|
435
|
+
connection: nextLive.connection,
|
|
436
|
+
});
|
|
437
|
+
await subscription.close();
|
|
438
|
+
subscription = next;
|
|
439
|
+
live = nextLive;
|
|
440
|
+
notifyReady(options, live);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
try {
|
|
443
|
+
options.onStatus?.("rpc-unavailable", { error: safeMessage(error) });
|
|
444
|
+
} catch {}
|
|
445
|
+
}
|
|
446
|
+
}).finally(() => {
|
|
447
|
+
checking = false;
|
|
448
|
+
});
|
|
449
|
+
}, Math.trunc(requestedInterval));
|
|
450
|
+
timer.unref?.();
|
|
451
|
+
const close = async () => {
|
|
452
|
+
if (stopped) return await closed;
|
|
453
|
+
stopped = true;
|
|
454
|
+
clearInterval(timer);
|
|
455
|
+
options.signal?.removeEventListener("abort", abort);
|
|
456
|
+
await serial(async () => {
|
|
457
|
+
await subscription.close();
|
|
458
|
+
settle();
|
|
459
|
+
});
|
|
460
|
+
};
|
|
461
|
+
const abort = () => {
|
|
462
|
+
void close();
|
|
463
|
+
};
|
|
464
|
+
if (options.signal?.aborted) await close();
|
|
465
|
+
else options.signal?.addEventListener("abort", abort, { once: true });
|
|
466
|
+
return {
|
|
467
|
+
addTokens: (tokens: string | readonly string[]) =>
|
|
468
|
+
serial(async () => {
|
|
469
|
+
if (stopped) throw new Error("Trade subscription is closed");
|
|
470
|
+
await subscription.addTokens(tokens);
|
|
471
|
+
}),
|
|
472
|
+
removeTokens: (tokens: string | readonly string[]) =>
|
|
473
|
+
serial(async () => {
|
|
474
|
+
if (!stopped) await subscription.removeTokens(tokens);
|
|
475
|
+
}),
|
|
476
|
+
hasToken: (token: string) => !stopped && subscription.hasToken(token),
|
|
477
|
+
listTokens: () => (stopped ? [] : subscription.listTokens()),
|
|
478
|
+
close,
|
|
479
|
+
closed,
|
|
480
|
+
} satisfies TradeSubscription;
|
|
481
|
+
}
|
|
389
482
|
|
|
390
483
|
export async function subscribeLaunches(options: SubscribeLaunchesOptions) {
|
|
391
|
-
const live = await liveConnection(options);
|
|
484
|
+
const live = await liveConnection(options);
|
|
392
485
|
notifyReady(options, live);
|
|
393
486
|
return await subscribeCoreLaunches({
|
|
394
487
|
...options,
|
|
@@ -397,44 +490,82 @@ export async function subscribeLaunches(options: SubscribeLaunchesOptions) {
|
|
|
397
490
|
}
|
|
398
491
|
|
|
399
492
|
export async function subscribeMigrations(options: SubscribeMigrationsOptions) {
|
|
400
|
-
const live = await liveConnection(options);
|
|
493
|
+
const live = await liveConnection(options);
|
|
401
494
|
notifyReady(options, live);
|
|
402
495
|
return await subscribeCoreMigrations({
|
|
403
496
|
...options,
|
|
404
497
|
connection: live.connection,
|
|
405
498
|
});
|
|
406
499
|
}
|
|
407
|
-
export async function listenTrades(
|
|
500
|
+
export async function listenTrades(
|
|
408
501
|
options: ListenTradesOptions,
|
|
409
502
|
): Promise<TradeListener> {
|
|
410
503
|
const callbacks = new Set<(event: TradeEvent) => void | Promise<void>>();
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
504
|
+
const history =
|
|
505
|
+
options.history === false
|
|
506
|
+
? undefined
|
|
507
|
+
: createLiveTradeHistoryRecorder({
|
|
508
|
+
...options.history,
|
|
509
|
+
dbPath: options.dbPath,
|
|
510
|
+
commitment: options.commitment,
|
|
511
|
+
onStatus: options.onStatus,
|
|
512
|
+
});
|
|
513
|
+
const pending: TradeEvent[] = [];
|
|
514
|
+
const migrationCallbacks = new Set<
|
|
515
|
+
(event: TradeListenerMigration) => void | Promise<void>
|
|
516
|
+
>();
|
|
517
|
+
const pendingMigrations: TradeListenerMigration[] = [];
|
|
518
|
+
const watched = new Set(options.tokens);
|
|
519
|
+
const lastPools = new Map<string, string | null>();
|
|
520
|
+
let subscription: TradeSubscription;
|
|
521
|
+
let migrations: Awaited<ReturnType<typeof subscribeMigrations>> | undefined;
|
|
522
|
+
let stopped = false;
|
|
523
|
+
let resolveClosed!: () => void;
|
|
524
|
+
let rejectClosed!: (error: unknown) => void;
|
|
525
|
+
const closed = new Promise<void>((resolve, reject) => {
|
|
526
|
+
resolveClosed = resolve;
|
|
527
|
+
rejectClosed = reject;
|
|
528
|
+
});
|
|
529
|
+
void closed.catch(() => undefined);
|
|
530
|
+
let tail: Promise<unknown> = Promise.resolve();
|
|
531
|
+
const serial = <T>(work: () => Promise<T>): Promise<T> => {
|
|
532
|
+
const job = tail.then(work, work);
|
|
533
|
+
tail = job.catch(() => undefined);
|
|
534
|
+
return job;
|
|
535
|
+
};
|
|
536
|
+
const onMigration = async (event: MigrationEvent) => {
|
|
537
|
+
if (stopped || !watched.has(event.mint)) return;
|
|
538
|
+
const migration = {
|
|
539
|
+
...event,
|
|
540
|
+
oldPool: lastPools.get(event.mint) ?? null,
|
|
541
|
+
newPool: event.pool,
|
|
542
|
+
};
|
|
543
|
+
lastPools.set(event.mint, event.pool);
|
|
544
|
+
await serial(async () => {
|
|
545
|
+
if (stopped || !watched.has(event.mint)) return;
|
|
546
|
+
// Mint mentions cover the new pool; rebuilding resets cached identities,
|
|
547
|
+
// decimals and supply without changing the caller's watched token list.
|
|
548
|
+
await subscription.removeTokens(event.mint);
|
|
549
|
+
await subscription.addTokens(event.mint);
|
|
550
|
+
});
|
|
551
|
+
if (!migrationCallbacks.size) {
|
|
552
|
+
pendingMigrations.push(migration);
|
|
553
|
+
if (pendingMigrations.length > 1000) pendingMigrations.shift();
|
|
554
|
+
} else
|
|
555
|
+
await Promise.all(
|
|
556
|
+
[...migrationCallbacks].map((callback) => callback(migration)),
|
|
557
|
+
);
|
|
558
|
+
};
|
|
559
|
+
const dispatch = async (event: TradeEvent) => {
|
|
560
|
+
if (!watched.has(event.mint) || stopped) return;
|
|
561
|
+
lastPools.set(event.mint, event.pool);
|
|
562
|
+
try {
|
|
563
|
+
await history?.record(event);
|
|
564
|
+
} catch (error) {
|
|
565
|
+
options.onStatus?.("history-persistence-error", {
|
|
566
|
+
error: safeMessage(error),
|
|
567
|
+
});
|
|
568
|
+
}
|
|
438
569
|
if (callbacks.size === 0) {
|
|
439
570
|
pending.push(event);
|
|
440
571
|
if (pending.length > 1_000) pending.shift();
|
|
@@ -446,30 +577,96 @@ export async function listenTrades(
|
|
|
446
577
|
}),
|
|
447
578
|
);
|
|
448
579
|
};
|
|
449
|
-
const onStatus = (event: string, data?: Record<string, unknown>) => {
|
|
450
|
-
options.onStatus?.(event, data);
|
|
451
|
-
if (event === "rpc-ready" && migrations && !stopped)
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
580
|
+
const onStatus = (event: string, data?: Record<string, unknown>) => {
|
|
581
|
+
options.onStatus?.(event, data);
|
|
582
|
+
if (event === "rpc-ready" && migrations && !stopped)
|
|
583
|
+
void serial(async () => {
|
|
584
|
+
if (stopped) return;
|
|
585
|
+
const next = await subscribeMigrations({
|
|
586
|
+
rpcUrls: options.rpcUrls,
|
|
587
|
+
tokens: [...watched],
|
|
588
|
+
commitment: options.commitment,
|
|
589
|
+
metadata: options.metadata,
|
|
590
|
+
onMigration,
|
|
591
|
+
onStatus: options.onStatus,
|
|
592
|
+
});
|
|
593
|
+
await migrations?.close();
|
|
594
|
+
migrations = next;
|
|
595
|
+
}).catch((error) =>
|
|
596
|
+
options.onStatus?.("migration-resubscribe-error", {
|
|
597
|
+
error: safeMessage(error),
|
|
598
|
+
}),
|
|
599
|
+
);
|
|
600
|
+
};
|
|
601
|
+
try {
|
|
602
|
+
subscription = await subscribeTrades({
|
|
603
|
+
...options,
|
|
604
|
+
signal: undefined,
|
|
605
|
+
onStatus,
|
|
606
|
+
onTrade: dispatch,
|
|
607
|
+
});
|
|
608
|
+
} catch (error) {
|
|
609
|
+
await history?.close();
|
|
610
|
+
throw error;
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
migrations = await subscribeMigrations({
|
|
614
|
+
rpcUrls: options.rpcUrls,
|
|
615
|
+
tokens: [...watched],
|
|
616
|
+
commitment: options.commitment,
|
|
617
|
+
metadata: options.metadata,
|
|
618
|
+
onMigration,
|
|
619
|
+
onStatus: options.onStatus,
|
|
620
|
+
});
|
|
621
|
+
} catch (error) {
|
|
622
|
+
await subscription.close();
|
|
623
|
+
await history?.close();
|
|
624
|
+
throw error;
|
|
625
|
+
}
|
|
626
|
+
const close = async () => {
|
|
627
|
+
if (stopped) return await closed;
|
|
628
|
+
stopped = true;
|
|
629
|
+
options.signal?.removeEventListener("abort", abort);
|
|
630
|
+
try {
|
|
631
|
+
await serial(async () => {
|
|
632
|
+
await Promise.all([subscription.close(), migrations?.close()]);
|
|
633
|
+
watched.clear();
|
|
634
|
+
await history?.close();
|
|
635
|
+
});
|
|
636
|
+
resolveClosed();
|
|
637
|
+
} catch (error) {
|
|
638
|
+
rejectClosed(error);
|
|
639
|
+
throw error;
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
const abort = () => {
|
|
643
|
+
void close();
|
|
644
|
+
};
|
|
645
|
+
if (options.signal?.aborted) await close();
|
|
646
|
+
else options.signal?.addEventListener("abort", abort, { once: true });
|
|
647
|
+
return {
|
|
648
|
+
add: (tokens) =>
|
|
649
|
+
serial(async () => {
|
|
650
|
+
if (stopped) throw new Error("Trade listener is closed");
|
|
651
|
+
const values = typeof tokens === "string" ? [tokens] : [...tokens];
|
|
652
|
+
await subscription.addTokens(values);
|
|
653
|
+
await migrations!.addTokens(values);
|
|
654
|
+
values.forEach((token) => watched.add(token));
|
|
655
|
+
}),
|
|
656
|
+
remove: (tokens) =>
|
|
657
|
+
serial(async () => {
|
|
658
|
+
const values = typeof tokens === "string" ? [tokens] : [...tokens];
|
|
659
|
+
values.forEach((token) => {
|
|
660
|
+
watched.delete(token);
|
|
661
|
+
lastPools.delete(token);
|
|
662
|
+
});
|
|
663
|
+
await Promise.all([
|
|
664
|
+
subscription.removeTokens(values),
|
|
665
|
+
migrations!.removeTokens(values),
|
|
666
|
+
]);
|
|
667
|
+
}),
|
|
668
|
+
has: (token) => !stopped && watched.has(token),
|
|
669
|
+
list: () => (stopped ? [] : [...watched]),
|
|
473
670
|
onTrade(callback) {
|
|
474
671
|
callbacks.add(callback);
|
|
475
672
|
if (pending.length) {
|
|
@@ -480,14 +677,19 @@ export async function listenTrades(
|
|
|
480
677
|
}
|
|
481
678
|
return () => callbacks.delete(callback);
|
|
482
679
|
},
|
|
483
|
-
onMigration(callback) {
|
|
484
|
-
migrationCallbacks.add(callback);
|
|
485
|
-
if (pendingMigrations.length) {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
680
|
+
onMigration(callback) {
|
|
681
|
+
migrationCallbacks.add(callback);
|
|
682
|
+
if (pendingMigrations.length) {
|
|
683
|
+
const rows = pendingMigrations.splice(0);
|
|
684
|
+
void Promise.resolve().then(async () => {
|
|
685
|
+
for (const event of rows) await callback(event);
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
return () => migrationCallbacks.delete(callback);
|
|
689
|
+
},
|
|
690
|
+
close,
|
|
489
691
|
get closed() {
|
|
490
|
-
return
|
|
692
|
+
return closed;
|
|
491
693
|
},
|
|
492
694
|
};
|
|
493
695
|
}
|