@mfxhub/sdk 0.0.0-stage → 0.1.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.
@@ -0,0 +1,5 @@
1
+ /**
2
+ * This file was auto-generated by openapi-typescript.
3
+ * Do not make direct changes to the file.
4
+ */
5
+ export {};
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The MFX Hub TypeScript SDK.
3
+ *
4
+ * ```ts
5
+ * import { MfxClient } from "@mfxhub/sdk";
6
+ * const client = new MfxClient("https://api.dev.mfxhub.com/v1", keyId, privateKey);
7
+ * ```
8
+ */
9
+ export { MfxClient, DEFAULT_READ_RETRIES, DEFAULT_TIMEOUT_MS } from "./client.js";
10
+ export type { Answer, Budget, ClientOptions, RequestSpec } from "./client.js";
11
+ export * from "./errors.js";
12
+ export { Page } from "./pages.js";
13
+ export { Catalog } from "./catalog.js";
14
+ export type { Quote, QuoteOptions } from "./catalog.js";
15
+ export { Execution } from "./execution.js";
16
+ export { CLOSE_LIFETIME, CLOSE_REAUTHENTICATE, CLOSE_SLOW_CONSUMER, Channel, ChannelRefused, MfxStream, Resync, StreamClosed, StreamMessage, wsConnector, } from "./stream.js";
17
+ export type { Connector, StreamConnection, StreamEvent, StreamOptions, StreamStats, Timeframe, } from "./stream.js";
18
+ export type { OrderType, PlaceOrder, Side } from "./execution.js";
19
+ export type { CommandState, ExecutionResult, Reason, Tickets } from "./results.js";
20
+ export { Terminals, isUsable } from "./terminals.js";
21
+ export type { CreateTerminal, PageOptions, Redundancy, Terminal, TerminalState, } from "./terminals.js";
22
+ export { RequestSigner, SigningKey, signingMessage } from "./signing.js";
23
+ export { KEY_HEADER, NONCE_HEADER, SIGNATURE_HEADER, TIMESTAMP_HEADER } from "./signing.js";
24
+ export type { components, operations, paths } from "./generated/api.js";
25
+ /** This package's version. */
26
+ export declare const VERSION = "0.1.0";
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The MFX Hub TypeScript SDK.
3
+ *
4
+ * ```ts
5
+ * import { MfxClient } from "@mfxhub/sdk";
6
+ * const client = new MfxClient("https://api.dev.mfxhub.com/v1", keyId, privateKey);
7
+ * ```
8
+ */
9
+ export { MfxClient, DEFAULT_READ_RETRIES, DEFAULT_TIMEOUT_MS } from "./client.js";
10
+ export * from "./errors.js";
11
+ export { Page } from "./pages.js";
12
+ export { Catalog } from "./catalog.js";
13
+ export { Execution } from "./execution.js";
14
+ export { CLOSE_LIFETIME, CLOSE_REAUTHENTICATE, CLOSE_SLOW_CONSUMER, Channel, ChannelRefused, MfxStream, Resync, StreamClosed, StreamMessage, wsConnector, } from "./stream.js";
15
+ export { Terminals, isUsable } from "./terminals.js";
16
+ export { RequestSigner, SigningKey, signingMessage } from "./signing.js";
17
+ export { KEY_HEADER, NONCE_HEADER, SIGNATURE_HEADER, TIMESTAMP_HEADER } from "./signing.js";
18
+ /** This package's version. */
19
+ export const VERSION = "0.1.0";
@@ -0,0 +1,22 @@
1
+ /** One page, and the cursor for the next. `nextCursor` is null on the last page. */
2
+ export declare class Page<T> implements Iterable<T> {
3
+ readonly items: readonly T[];
4
+ readonly nextCursor: string | null;
5
+ readonly returned: number | null;
6
+ /**
7
+ * @param items this page's items
8
+ * @param nextCursor pass it back as `cursor` for the next page; null on the last
9
+ * @param returned how many are in THIS page, when the server says. Not a total.
10
+ */
11
+ constructor(items: readonly T[], nextCursor: string | null, returned: number | null);
12
+ /** How many are in this page. Not a total, and never to be shown as one. */
13
+ get length(): number;
14
+ [Symbol.iterator](): Iterator<T>;
15
+ }
16
+ /**
17
+ * The page an answer holds. An empty cursor reads as the last page, because an empty string
18
+ * passed back as a cursor would fetch the first page again for ever.
19
+ */
20
+ export declare function pageOf<T>(body: unknown): Page<T>;
21
+ /** Every item of every page, one request a page, following the cursor until it is null. */
22
+ export declare function everyItem<T>(read: (cursor: string | undefined) => Promise<Page<T>>): AsyncGenerator<T, void, undefined>;
package/dist/pages.js ADDED
@@ -0,0 +1,46 @@
1
+ // Cursor pagination, in one place.
2
+ //
3
+ // Every list in this API is cursor-paginated, and there is no total, no page number and no offset:
4
+ // the API cannot answer those cheaply, so the SDK follows the cursor and never pretends to know
5
+ // how many there are.
6
+ /** One page, and the cursor for the next. `nextCursor` is null on the last page. */
7
+ export class Page {
8
+ items;
9
+ nextCursor;
10
+ returned;
11
+ /**
12
+ * @param items this page's items
13
+ * @param nextCursor pass it back as `cursor` for the next page; null on the last
14
+ * @param returned how many are in THIS page, when the server says. Not a total.
15
+ */
16
+ constructor(items, nextCursor, returned) {
17
+ this.items = items;
18
+ this.nextCursor = nextCursor;
19
+ this.returned = returned;
20
+ }
21
+ /** How many are in this page. Not a total, and never to be shown as one. */
22
+ get length() {
23
+ return this.items.length;
24
+ }
25
+ [Symbol.iterator]() {
26
+ return this.items[Symbol.iterator]();
27
+ }
28
+ }
29
+ /**
30
+ * The page an answer holds. An empty cursor reads as the last page, because an empty string
31
+ * passed back as a cursor would fetch the first page again for ever.
32
+ */
33
+ export function pageOf(body) {
34
+ const page = (typeof body === "object" && body !== null ? body : {});
35
+ const cursor = page.next_cursor;
36
+ return new Page(page.items ?? [], typeof cursor === "string" && cursor !== "" ? cursor : null, typeof page.returned === "number" ? page.returned : null);
37
+ }
38
+ /** Every item of every page, one request a page, following the cursor until it is null. */
39
+ export async function* everyItem(read) {
40
+ let cursor;
41
+ do {
42
+ const page = await read(cursor);
43
+ yield* page.items;
44
+ cursor = page.nextCursor ?? undefined;
45
+ } while (cursor !== undefined);
46
+ }
@@ -0,0 +1,78 @@
1
+ import type { components } from "./generated/api.js";
2
+ /** Where a command is. Final: SUCCEEDED, FAILED, EXPIRED, REJECTED. UNKNOWN is not final. */
3
+ export type CommandState = components["schemas"]["CommandState"];
4
+ /**
5
+ * Why a command ended as it did: the platform's reasons, and four the SDK concludes itself
6
+ * (`transport_error`, `timeout`, `refused`, and `maintenance` when the API refuses outright).
7
+ * A reason the platform adds later arrives in `reasonText` as it was sent, with `reason` null.
8
+ */
9
+ export type Reason = "success" | "requote" | "instrument_disabled" | "no_liquidity" | "market_closed" | "invalid_stops" | "invalid_volume" | "insufficient_funds" | "trade_disabled" | "rejected_other" | "rate_limited" | "expired" | "unknown" | "maintenance" | "transport_error" | "timeout" | "refused";
10
+ /** MT5 relates one order, one deal and one position per fill; null where MT5 has none yet. */
11
+ export interface Tickets {
12
+ readonly order: number | null;
13
+ readonly deal: number | null;
14
+ readonly position: number | null;
15
+ }
16
+ /**
17
+ * The whole answer to one execution call. Never thrown, always returned.
18
+ *
19
+ * Read `state` first: SUCCEEDED is done; FAILED, EXPIRED and REJECTED did not happen; UNKNOWN may
20
+ * already be a position and must never be sent again under a new key.
21
+ */
22
+ export interface ExecutionResult {
23
+ /** True when the command succeeded. */
24
+ readonly ok: boolean;
25
+ /** The key this call was made with, so a retry can reuse it without bookkeeping. */
26
+ readonly idempotencyKey: string;
27
+ /** How long the SDK waited, end to end, in milliseconds. Always known. */
28
+ readonly clientRttMs: number;
29
+ /** The platform's id for the command. */
30
+ readonly commandId: string | null;
31
+ readonly state: CommandState | null;
32
+ /** True once the platform will not change this answer again. */
33
+ readonly final: boolean;
34
+ /** Whether sending the same order again under a NEW key can succeed. False while UNKNOWN. */
35
+ readonly retryable: boolean;
36
+ /** The same key with the same body: this is the original command, not a second one. */
37
+ readonly duplicate: boolean;
38
+ /** Why it ended as it did; null when the platform gives none, as on a success. */
39
+ readonly reason: Reason | null;
40
+ /** The reason exactly as it arrived, including one this SDK version does not know. */
41
+ readonly reasonText: string | null;
42
+ /** The trading platform's own return code, and its meaning in words. */
43
+ readonly mt5Retcode: number | null;
44
+ readonly retcodeText: string | null;
45
+ /** The broker's own comment, verbatim. */
46
+ readonly commentRaw: string | null;
47
+ /** The terminal's state when the command was answered. */
48
+ readonly terminalState: string | null;
49
+ readonly tickets: Tickets;
50
+ /** The price and volume the broker filled at, as decimal strings. */
51
+ readonly execPrice: string | null;
52
+ readonly execVolume: string | null;
53
+ readonly brokerLatencyMs: number | null;
54
+ readonly agentLatencyMs: number | null;
55
+ /** How many times the platform sent it to the broker. */
56
+ readonly attempts: number;
57
+ /** The terminal a failover moved this command from, when one did. */
58
+ readonly reroutedFrom: string | null;
59
+ /** What the platform or the SDK said when the request was refused outright. */
60
+ readonly detail: string | null;
61
+ /** When sending it again can work: a maintenance window's end, where the platform said. */
62
+ readonly retryAt: Date | null;
63
+ }
64
+ /** A result from a Command or a CommandAccepted body, whichever the call answered with. */
65
+ export declare function fromCommand(body: unknown, idempotencyKey: string, clientRttMs: number): ExecutionResult;
66
+ /** Why the SDK itself concluded a result: no answer, no answer in time, or a refusal. */
67
+ export type Concluded = "transport_error" | "timeout" | "refused" | "rate_limited" | "maintenance";
68
+ /**
69
+ * A result the SDK concluded itself.
70
+ *
71
+ * `mayHaveReached` is for a transport error after the request was sent: the order may be a
72
+ * position, so the result reads UNKNOWN and is not retryable under a new key. Sending it again
73
+ * under the SAME key is safe, and answers with the original if it arrived.
74
+ */
75
+ export declare function concluded(reason: Concluded, idempotencyKey: string, clientRttMs: number, detail: string, options?: {
76
+ readonly mayHaveReached?: boolean;
77
+ readonly retryAt?: Date | null;
78
+ }): ExecutionResult;
@@ -0,0 +1,119 @@
1
+ // What an execution call answers, and the reasons it can carry.
2
+ //
3
+ // Execution never throws across this boundary. A transport fault, a timeout, a rate limit and a
4
+ // broker rejection are all results, because a trading loop that has to catch exceptions to find
5
+ // out whether it has a position is a trading loop that will one day place two.
6
+ //
7
+ // UNKNOWN is not a failure and must never be sent again under a new key: the order may have
8
+ // reached the broker and its outcome could not be read. `retryable` is false while a command is
9
+ // UNKNOWN, and that is the flag to act on, never the reason or the state read by hand.
10
+ import { decimalOut } from "./decimals.js";
11
+ const STATES = new Set([
12
+ "ACCEPTED",
13
+ "ENQUEUED",
14
+ "DELIVERED",
15
+ "EXECUTING",
16
+ "SUCCEEDED",
17
+ "FAILED",
18
+ "EXPIRED",
19
+ "REJECTED",
20
+ "UNKNOWN",
21
+ ]);
22
+ const FINAL = new Set([
23
+ "SUCCEEDED",
24
+ "FAILED",
25
+ "EXPIRED",
26
+ "REJECTED",
27
+ ]);
28
+ const REASONS = new Set([
29
+ "success",
30
+ "requote",
31
+ "instrument_disabled",
32
+ "no_liquidity",
33
+ "market_closed",
34
+ "invalid_stops",
35
+ "invalid_volume",
36
+ "insufficient_funds",
37
+ "trade_disabled",
38
+ "rejected_other",
39
+ "rate_limited",
40
+ "expired",
41
+ "unknown",
42
+ "maintenance",
43
+ "transport_error",
44
+ "timeout",
45
+ "refused",
46
+ ]);
47
+ function int(value) {
48
+ return typeof value === "number" && Number.isInteger(value) ? value : null;
49
+ }
50
+ function text(value) {
51
+ return typeof value === "string" && value !== "" ? value : null;
52
+ }
53
+ function record(value) {
54
+ return typeof value === "object" && value !== null && !Array.isArray(value)
55
+ ? value
56
+ : {};
57
+ }
58
+ /** A result from a Command or a CommandAccepted body, whichever the call answered with. */
59
+ export function fromCommand(body, idempotencyKey, clientRttMs) {
60
+ const raw = record(body);
61
+ const state = typeof raw.state === "string" && STATES.has(raw.state) ? raw.state : null;
62
+ const result = record(raw.result);
63
+ const reasonText = text(result.reason);
64
+ const tickets = record(result.tickets);
65
+ return {
66
+ ok: result.ok === true,
67
+ idempotencyKey,
68
+ clientRttMs,
69
+ commandId: text(raw.command_id),
70
+ state: state,
71
+ final: typeof raw.final === "boolean" ? raw.final : state !== null && FINAL.has(state),
72
+ // Never retryable while the outcome is unread: that is how one fill becomes two. A
73
+ // maintenance refusal is, whatever the flag says: it was refused before it ran.
74
+ retryable: (result.retryable === true || reasonText === "maintenance") && state !== "UNKNOWN",
75
+ duplicate: raw.duplicate === true,
76
+ reason: reasonText !== null && REASONS.has(reasonText) ? reasonText : null,
77
+ reasonText,
78
+ mt5Retcode: int(result.mt5_retcode),
79
+ retcodeText: text(result.retcode_text),
80
+ commentRaw: text(result.comment_raw),
81
+ terminalState: text(result.terminal_state),
82
+ tickets: {
83
+ order: int(tickets.order),
84
+ deal: int(tickets.deal),
85
+ position: int(tickets.position),
86
+ },
87
+ execPrice: decimalOut(result.exec_price),
88
+ execVolume: decimalOut(result.exec_volume),
89
+ brokerLatencyMs: int(result.broker_latency_ms),
90
+ agentLatencyMs: int(result.agent_latency_ms),
91
+ attempts: int(result.attempts) ?? 0,
92
+ reroutedFrom: text(result.rerouted_from),
93
+ detail: null,
94
+ retryAt: null,
95
+ };
96
+ }
97
+ /**
98
+ * A result the SDK concluded itself.
99
+ *
100
+ * `mayHaveReached` is for a transport error after the request was sent: the order may be a
101
+ * position, so the result reads UNKNOWN and is not retryable under a new key. Sending it again
102
+ * under the SAME key is safe, and answers with the original if it arrived.
103
+ */
104
+ export function concluded(reason, idempotencyKey, clientRttMs, detail, options = {}) {
105
+ const reached = reason === "transport_error" && options.mayHaveReached === true;
106
+ return {
107
+ ...fromCommand({}, idempotencyKey, clientRttMs),
108
+ reason,
109
+ reasonText: reason,
110
+ // A timeout is never retryable: the order may have been placed. A transport error before
111
+ // the request left is, a rate limit is, and so is a maintenance refusal once the window ends.
112
+ retryable: !reached &&
113
+ (reason === "transport_error" || reason === "rate_limited" || reason === "maintenance"),
114
+ state: reached ? "UNKNOWN" : null,
115
+ final: true,
116
+ detail,
117
+ retryAt: options.retryAt ?? null,
118
+ };
119
+ }
@@ -0,0 +1,34 @@
1
+ import { inspect } from "node:util";
2
+ export declare const KEY_HEADER = "X-MFX-Key";
3
+ export declare const TIMESTAMP_HEADER = "X-MFX-Timestamp";
4
+ export declare const NONCE_HEADER = "X-MFX-Nonce";
5
+ export declare const SIGNATURE_HEADER = "X-MFX-Signature";
6
+ /** A tenant's Ed25519 private key, which renders as `***` wherever it could be printed. */
7
+ export declare class SigningKey {
8
+ #private;
9
+ /** Accepts PEM, the base64 of the 32 raw bytes, the bytes themselves, or another SigningKey. */
10
+ constructor(key: string | Uint8Array | SigningKey);
11
+ /** The base64 signature of `message`. */
12
+ sign(message: Uint8Array): string;
13
+ toString(): string;
14
+ toJSON(): string;
15
+ [inspect.custom](): string;
16
+ }
17
+ /** The exact bytes a request's signature covers. Refuses a nonce that could move the lines. */
18
+ export declare function signingMessage(timestamp: string, nonce: string, method: string, pathWithQuery: string, body: Uint8Array): Uint8Array;
19
+ /** Makes the four headers for one request, with a fresh timestamp and nonce each time. */
20
+ export declare class RequestSigner {
21
+ #private;
22
+ readonly keyId: string;
23
+ private readonly clock;
24
+ private readonly nonce;
25
+ /**
26
+ * @param keyId the key's id, safe to log
27
+ * @param key the private key
28
+ * @param clock milliseconds since the epoch; replaceable in tests
29
+ * @param nonce a fresh nonce; replaceable in tests
30
+ */
31
+ constructor(keyId: string, key: string | Uint8Array | SigningKey, clock?: () => number, nonce?: () => string);
32
+ /** The headers for one request: the stream's upgrade is signed through here too. */
33
+ headersFor(method: string, pathWithQuery: string, body?: Uint8Array): Record<string, string>;
34
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * The four headers every MFX Hub request carries, and the key that makes them.
3
+ *
4
+ * A request is signed, not carrying a token: `X-MFX-Key` names the key, `X-MFX-Timestamp` and
5
+ * `X-MFX-Nonce` make each request usable once, and `X-MFX-Signature` is an Ed25519 signature over
6
+ *
7
+ * timestamp_ms \n nonce \n METHOD \n path_with_query \n sha256_hex(body)
8
+ *
9
+ * base64-encoded. The timestamp is epoch milliseconds and must be within 30 s of the server's
10
+ * clock; the nonce is unique per key for five minutes, so a retry is always signed again.
11
+ *
12
+ * The private key is shown once when the key is issued and the platform never stores it. A
13
+ * {@link SigningKey} prints as `***` everywhere it could be printed, so it cannot reach a log.
14
+ */
15
+ import { createHash, createPrivateKey, randomBytes, sign } from "node:crypto";
16
+ import { inspect } from "node:util";
17
+ export const KEY_HEADER = "X-MFX-Key";
18
+ export const TIMESTAMP_HEADER = "X-MFX-Timestamp";
19
+ export const NONCE_HEADER = "X-MFX-Nonce";
20
+ export const SIGNATURE_HEADER = "X-MFX-Signature";
21
+ /** What the platform accepts as a nonce: it is a line of the signed message. */
22
+ const NONCE = /^[A-Za-z0-9_-]{16,64}$/;
23
+ /** PKCS#8 wrapping for a raw 32-byte Ed25519 private key. */
24
+ const PKCS8_ED25519_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
25
+ const SEED_BYTES = 32;
26
+ function load(material) {
27
+ const text = typeof material === "string" ? material.trim() : null;
28
+ if (text?.startsWith("-----BEGIN")) {
29
+ const key = createPrivateKey(text);
30
+ if (key.asymmetricKeyType !== "ed25519") {
31
+ throw new TypeError(`an MFX Hub key is Ed25519, not ${String(key.asymmetricKeyType)}`);
32
+ }
33
+ return key;
34
+ }
35
+ const raw = text === null ? Buffer.from(material) : Buffer.from(text, "base64");
36
+ if (raw.length !== SEED_BYTES || (text !== null && raw.toString("base64") !== text)) {
37
+ throw new TypeError("a signing key is a PEM private key, or the base64 of its 32 raw bytes, or those bytes");
38
+ }
39
+ return createPrivateKey({
40
+ key: Buffer.concat([PKCS8_ED25519_PREFIX, raw]),
41
+ format: "der",
42
+ type: "pkcs8",
43
+ });
44
+ }
45
+ /** A tenant's Ed25519 private key, which renders as `***` wherever it could be printed. */
46
+ export class SigningKey {
47
+ #key;
48
+ /** Accepts PEM, the base64 of the 32 raw bytes, the bytes themselves, or another SigningKey. */
49
+ constructor(key) {
50
+ this.#key = key instanceof SigningKey ? key.#key : load(key);
51
+ }
52
+ /** The base64 signature of `message`. */
53
+ sign(message) {
54
+ return sign(null, message, this.#key).toString("base64");
55
+ }
56
+ toString() {
57
+ return "SigningKey(***)";
58
+ }
59
+ toJSON() {
60
+ return "***";
61
+ }
62
+ [inspect.custom]() {
63
+ return "SigningKey(***)";
64
+ }
65
+ }
66
+ /** The exact bytes a request's signature covers. Refuses a nonce that could move the lines. */
67
+ export function signingMessage(timestamp, nonce, method, pathWithQuery, body) {
68
+ if (!NONCE.test(nonce)) {
69
+ throw new RangeError(`a nonce is 16-64 characters of A-Z a-z 0-9 _ -; got ${nonce.length} characters`);
70
+ }
71
+ const digest = createHash("sha256").update(body).digest("hex");
72
+ return Buffer.from([timestamp, nonce, method.toUpperCase(), pathWithQuery, digest].join("\n"));
73
+ }
74
+ /** Makes the four headers for one request, with a fresh timestamp and nonce each time. */
75
+ export class RequestSigner {
76
+ keyId;
77
+ clock;
78
+ nonce;
79
+ /**
80
+ * @param keyId the key's id, safe to log
81
+ * @param key the private key
82
+ * @param clock milliseconds since the epoch; replaceable in tests
83
+ * @param nonce a fresh nonce; replaceable in tests
84
+ */
85
+ constructor(keyId, key, clock = Date.now, nonce = () => randomBytes(16).toString("base64url")) {
86
+ this.keyId = keyId;
87
+ this.clock = clock;
88
+ this.nonce = nonce;
89
+ this.#key = new SigningKey(key);
90
+ }
91
+ #key;
92
+ /** The headers for one request: the stream's upgrade is signed through here too. */
93
+ headersFor(method, pathWithQuery, body = new Uint8Array()) {
94
+ const timestamp = String(Math.trunc(this.clock()));
95
+ const nonce = this.nonce();
96
+ return {
97
+ [KEY_HEADER]: this.keyId,
98
+ [TIMESTAMP_HEADER]: timestamp,
99
+ [NONCE_HEADER]: nonce,
100
+ [SIGNATURE_HEADER]: this.#key.sign(signingMessage(timestamp, nonce, method, pathWithQuery, body)),
101
+ };
102
+ }
103
+ }
@@ -0,0 +1,139 @@
1
+ import type { MfxClient } from "./client.js";
2
+ import { MfxError } from "./errors.js";
3
+ /** The server says why it hung up. 4001 is the one reconnecting cannot fix. */
4
+ export declare const CLOSE_REAUTHENTICATE = 4001;
5
+ export declare const CLOSE_SLOW_CONSUMER = 4008;
6
+ export declare const CLOSE_LIFETIME = 4009;
7
+ /** The timeframes closed candles are published for. */
8
+ export type Timeframe = "M1" | "M5" | "M15" | "M30" | "H1" | "H4" | "D1" | "W1" | "MN1";
9
+ /** Builds the channel keys the platform accepts, from ids rather than names. */
10
+ export declare const Channel: {
11
+ /** Every platform event for this tenant. */
12
+ readonly events: "events";
13
+ /** Ticks for one symbol on one server, in the broker's own name for it. */
14
+ readonly md: (serverId: string, symbol: string) => string;
15
+ /** Closed candles. Only closed ones are published; a forming candle is not a fact yet. */
16
+ readonly candles: (serverId: string, symbol: string, timeframe: Timeframe) => string;
17
+ /** Execution results for one terminal: where a command id is finally answered. */
18
+ readonly results: (terminalId: string) => string;
19
+ /** One terminal's live account: a snapshot, then marks. */
20
+ readonly account: (terminalId: string) => string;
21
+ /** What the tenant's own expert advisor on this terminal writes. Needs `ea:read`. */
22
+ readonly ea: (terminalId: string) => string;
23
+ /** The symbol's order book, whole, each time it changes. */
24
+ readonly book: (serverId: string, symbol: string) => string;
25
+ };
26
+ /** The connection ended. `code` is the WebSocket close code, `reason` the server's words. */
27
+ export declare class StreamClosed extends MfxError {
28
+ readonly code: number;
29
+ readonly reason: string;
30
+ name: string;
31
+ constructor(code: number, reason?: string);
32
+ }
33
+ /** One message on one channel. */
34
+ export declare class StreamMessage {
35
+ readonly channel: string;
36
+ /** Increases per channel; a jump is counted in `stats.dropped`. */
37
+ readonly seq: number | null;
38
+ /** The event type on `events` and `results` (`command.completed`); null on a tick or candle. */
39
+ readonly type: string | null;
40
+ readonly data: Readonly<Record<string, unknown>>;
41
+ /** `account:` only: the seq of the snapshot this message applies to. */
42
+ readonly base: number | null;
43
+ constructor(channel: string,
44
+ /** Increases per channel; a jump is counted in `stats.dropped`. */
45
+ seq: number | null,
46
+ /** The event type on `events` and `results` (`command.completed`); null on a tick or candle. */
47
+ type: string | null, data: Readonly<Record<string, unknown>>,
48
+ /** `account:` only: the seq of the snapshot this message applies to. */
49
+ base?: number | null);
50
+ }
51
+ /** The position asked for is older than the replay window: read the current state over REST. */
52
+ export declare class Resync {
53
+ readonly channel: string;
54
+ constructor(channel: string);
55
+ }
56
+ /**
57
+ * The platform refused one channel; every other channel carries on. The subscription is dropped,
58
+ * so a reconnect does not ask for it again and collect the same refusal for ever.
59
+ */
60
+ export declare class ChannelRefused {
61
+ readonly channel: string;
62
+ /** The platform's own code: `not_found`, `forbidden`, `not_subscribed`. */
63
+ readonly code: string;
64
+ readonly message: string;
65
+ readonly details: Readonly<Record<string, unknown>>;
66
+ constructor(channel: string,
67
+ /** The platform's own code: `not_found`, `forbidden`, `not_subscribed`. */
68
+ code: string, message: string, details?: Readonly<Record<string, unknown>>);
69
+ }
70
+ /** What the stream delivers. Resync and ChannelRefused are delivered, not thrown. */
71
+ export type StreamEvent = StreamMessage | Resync | ChannelRefused;
72
+ /** What the stream missed and how often it had to come back. */
73
+ export interface StreamStats {
74
+ /** Connections opened after the first one. */
75
+ readonly reconnects: number;
76
+ /** Messages the sequence numbers prove were not delivered, over every channel. */
77
+ readonly dropped: number;
78
+ /** Channels told to resync because the replay window had passed. */
79
+ readonly resyncs: number;
80
+ /** Times the server closed the stream for falling behind (4008). */
81
+ readonly slowConsumerDrops: number;
82
+ /** Times the 24-hour connection lifetime was reached (4009). Expected, not a fault. */
83
+ readonly lifetimeReconnects: number;
84
+ /** Channels the platform refused. Each cost its own channel and nothing else. */
85
+ readonly refused: number;
86
+ }
87
+ /** What this module needs of a WebSocket, and nothing more. */
88
+ export interface StreamConnection {
89
+ send(text: string): Promise<void>;
90
+ /** The next frame; rejects with StreamClosed once the connection has ended. */
91
+ recv(): Promise<string>;
92
+ close(): Promise<void>;
93
+ }
94
+ /** Opens one connection with the signed upgrade headers. */
95
+ export type Connector = (url: string, headers: Readonly<Record<string, string>>) => Promise<StreamConnection>;
96
+ /** What {@link MfxClient.stream} takes. */
97
+ export interface StreamOptions {
98
+ /** Replaces the transport; a test drives reconnects with it. */
99
+ readonly connect?: Connector;
100
+ /** The stream's address; derived from the client's base URL by default. */
101
+ readonly url?: string;
102
+ /** Stop after this many reconnects; unlimited by default. */
103
+ readonly maxReconnects?: number;
104
+ }
105
+ /** The default transport, over `ws`, with backpressure: the socket pauses while frames wait. */
106
+ export declare const wsConnector: Connector;
107
+ /** The stream's address beside the REST base: `api.dev.mfxhub.com/v1` is `ws.dev.mfxhub.com/v1/stream`. */
108
+ export declare function streamUrl(baseUrl: string): string;
109
+ /** The multiplexed stream. Opened by `await client.stream()`; closed by `close()`. */
110
+ export declare class MfxStream implements AsyncIterable<StreamEvent> {
111
+ #private;
112
+ /** Use {@link MfxClient.stream}. */
113
+ constructor(client: MfxClient, options?: StreamOptions);
114
+ /** What has been missed so far. A snapshot; it never changes under the caller. */
115
+ get stats(): StreamStats;
116
+ /** The channels currently subscribed, in the order they were asked for. */
117
+ get channels(): readonly string[];
118
+ /** Opens the connection. {@link MfxClient.stream} does this for you. */
119
+ open(): Promise<this>;
120
+ /** Subscribe to one channel. Subscribing twice is not an error. */
121
+ subscribe(channel: string): Promise<void>;
122
+ /**
123
+ * Bring one channel back up to date from the last seq seen, when the caller has noticed from
124
+ * the data itself that it is behind. On a channel that does not replay, or one with no seq yet,
125
+ * this subscribes instead: there is no position to resume from.
126
+ */
127
+ resume(channel: string): Promise<void>;
128
+ /** Stop watching a channel, and stop subscribing to it again after a reconnect. */
129
+ unsubscribe(channel: string): Promise<void>;
130
+ /** Close the connection, if one is open. */
131
+ close(): Promise<void>;
132
+ [Symbol.asyncIterator](): AsyncGenerator<StreamEvent, void, undefined>;
133
+ /**
134
+ * The next event, reconnecting underneath as needed; null once the stream is over. Throws an
135
+ * AuthError on 4001, and the platform's typed error for an error that names no channel.
136
+ */
137
+ next(): Promise<StreamEvent | null>;
138
+ toString(): string;
139
+ }