@totalis/hip4-client 0.0.0-stage → 0.73.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 (74) hide show
  1. package/CHANGELOG.md +1903 -0
  2. package/LICENSE +202 -0
  3. package/README.md +143 -2
  4. package/dist/api-keys.d.ts +118 -0
  5. package/dist/api-keys.js +104 -0
  6. package/dist/audience-contract.d.ts +52 -0
  7. package/dist/audience-contract.js +1 -0
  8. package/dist/base.d.ts +41 -0
  9. package/dist/base.js +60 -0
  10. package/dist/client.d.ts +16 -0
  11. package/dist/client.js +15 -0
  12. package/dist/controller.d.ts +53 -0
  13. package/dist/controller.js +63 -0
  14. package/dist/errors.d.ts +3 -0
  15. package/dist/errors.js +3 -0
  16. package/dist/flow-errors.d.ts +62 -0
  17. package/dist/flow-errors.js +70 -0
  18. package/dist/generated/account-read.d.ts +2015 -0
  19. package/dist/generated/account-read.js +1 -0
  20. package/dist/generated/api-keys.d.ts +1303 -0
  21. package/dist/generated/api-keys.js +1 -0
  22. package/dist/generated/browse.d.ts +965 -0
  23. package/dist/generated/browse.js +1 -0
  24. package/dist/generated/client.d.ts +104 -0
  25. package/dist/generated/client.js +1 -0
  26. package/dist/generated/errors.d.ts +467 -0
  27. package/dist/generated/errors.js +1792 -0
  28. package/dist/generated/maker.d.ts +47 -0
  29. package/dist/generated/maker.js +1 -0
  30. package/dist/generated/openapi.d.ts +10867 -0
  31. package/dist/generated/openapi.js +1 -0
  32. package/dist/generated/operator.d.ts +7 -0
  33. package/dist/generated/operator.js +1 -0
  34. package/dist/generated/realtime.d.ts +1486 -0
  35. package/dist/generated/realtime.js +1 -0
  36. package/dist/generated/vault-abi.d.ts +49 -0
  37. package/dist/generated/vault-abi.js +70 -0
  38. package/dist/generated/version.d.ts +24 -0
  39. package/dist/generated/version.js +183 -0
  40. package/dist/hypercore.d.ts +111 -0
  41. package/dist/hypercore.js +132 -0
  42. package/dist/maker.d.ts +206 -0
  43. package/dist/maker.js +608 -0
  44. package/dist/money.d.ts +59 -0
  45. package/dist/money.js +66 -0
  46. package/dist/operator.d.ts +4 -0
  47. package/dist/operator.js +4 -0
  48. package/dist/pagination.d.ts +20 -0
  49. package/dist/pagination.js +27 -0
  50. package/dist/partner.d.ts +71 -0
  51. package/dist/partner.js +66 -0
  52. package/dist/realtime.d.ts +19 -0
  53. package/dist/realtime.js +10 -0
  54. package/dist/request.d.ts +115 -0
  55. package/dist/request.js +190 -0
  56. package/dist/signer.d.ts +51 -0
  57. package/dist/signer.js +90 -0
  58. package/dist/stream.d.ts +142 -0
  59. package/dist/stream.js +331 -0
  60. package/dist/totalis.d.ts +719 -0
  61. package/dist/totalis.js +1251 -0
  62. package/dist/typed-data.d.ts +330 -0
  63. package/dist/typed-data.js +340 -0
  64. package/dist/ui.d.ts +54 -0
  65. package/dist/ui.js +77 -0
  66. package/dist/wallet-session.d.ts +52 -0
  67. package/dist/wallet-session.js +63 -0
  68. package/examples/browser-wallet.html +69 -0
  69. package/examples/browser-wallet.mjs +57 -0
  70. package/examples/node-bot.mjs +57 -0
  71. package/examples/onboard.mjs +132 -0
  72. package/examples/partner-frontend.mjs +44 -0
  73. package/examples/partner-server.mjs +22 -0
  74. package/package.json +105 -4
package/dist/signer.js ADDED
@@ -0,0 +1,90 @@
1
+ import { address, canonicalSignature, domainTypes } from "./typed-data.js";
2
+ function withoutDomainType(types) {
3
+ const { EIP712Domain: _, ...rest } = types;
4
+ return rest;
5
+ }
6
+ /** JSON for eth_signTypedData_v4: decimal strings for integers, and the domain type the method requires. */
7
+ function typedDataJson(typedData) {
8
+ return JSON.stringify({
9
+ ...typedData,
10
+ types: {
11
+ EIP712Domain: domainTypes.EIP712Domain,
12
+ ...withoutDomainType(typedData.types),
13
+ },
14
+ }, (_, value) => (typeof value === "bigint" ? value.toString() : value));
15
+ }
16
+ function hexUtf8(message) {
17
+ return `0x${Array.from(new TextEncoder().encode(message), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
18
+ }
19
+ /** Wallets answer with `unknown`; accept only a hex string. */
20
+ function hexString(value) {
21
+ if (typeof value !== "string" || !value.startsWith("0x"))
22
+ throw new TypeError("the wallet did not return a hex value");
23
+ return value;
24
+ }
25
+ function firstAddress(value) {
26
+ return Array.isArray(value) && value.length > 0 ? hexString(value[0]) : undefined;
27
+ }
28
+ /**
29
+ * Adapt a viem account or wallet client, an EIP-1193 provider, or an ethers
30
+ * signer. Signatures come back canonical (lowercase, v in {27, 28}).
31
+ */
32
+ export function toTotalisSigner(input) {
33
+ const has = (name) => name in input && typeof input[name] === "function";
34
+ let signer;
35
+ if ("address" in input && typeof input.address === "function") {
36
+ signer = input;
37
+ }
38
+ else if ("getAddress" in input) {
39
+ const sign = (input.signTypedData ?? input._signTypedData)?.bind(input);
40
+ if (sign === undefined)
41
+ throw new TypeError("the ethers signer cannot sign typed data");
42
+ signer = {
43
+ address: () => input.getAddress(),
44
+ signTypedData: (typedData) => sign(typedData.domain, withoutDomainType(typedData.types), typedData.message),
45
+ signMessage: (message) => input.signMessage(message),
46
+ };
47
+ }
48
+ else if ("getAddresses" in input) {
49
+ // A hoisted account (possibly local) signs itself; otherwise the wallet's first address signs over RPC.
50
+ const account = async () => input.account ?? (await input.getAddresses())[0] ?? Promise.reject(new Error("wallet client has no account"));
51
+ // viem's generic signatures are declared `never`; widen them once here.
52
+ const signTypedData = input.signTypedData.bind(input);
53
+ const signMessage = input.signMessage.bind(input);
54
+ signer = {
55
+ address: async () => {
56
+ const current = await account();
57
+ return typeof current === "string" ? current : current.address;
58
+ },
59
+ signTypedData: async (typedData) => hexString(await signTypedData({ account: await account(), ...typedData })),
60
+ signMessage: async (message) => hexString(await signMessage({ account: await account(), message })),
61
+ };
62
+ }
63
+ else if ("address" in input && typeof input.address === "string") {
64
+ const account = input;
65
+ signer = {
66
+ address: async () => account.address,
67
+ signTypedData: (typedData) => account.signTypedData(typedData),
68
+ signMessage: (message) => account.signMessage({ message }),
69
+ };
70
+ }
71
+ else if (has("request")) {
72
+ // The provider's generic request is declared `never`; widen it once here.
73
+ const request = input.request.bind(input);
74
+ const account = async () => firstAddress(await request({ method: "eth_accounts" })) ??
75
+ firstAddress(await request({ method: "eth_requestAccounts" })) ?? Promise.reject(new Error("provider has no account"));
76
+ signer = {
77
+ address: account,
78
+ signTypedData: async (typedData) => hexString(await request({ method: "eth_signTypedData_v4", params: [await account(), typedDataJson(typedData)] })),
79
+ signMessage: async (message) => hexString(await request({ method: "personal_sign", params: [hexUtf8(message), await account()] })),
80
+ };
81
+ }
82
+ else {
83
+ throw new TypeError("unsupported signer: pass a viem account or wallet client, an EIP-1193 provider, or an ethers signer");
84
+ }
85
+ return {
86
+ address: async () => address((await signer.address()).toLowerCase()),
87
+ signTypedData: async (typedData) => canonicalSignature(await signer.signTypedData(typedData)),
88
+ signMessage: async (message) => canonicalSignature(await signer.signMessage(message)),
89
+ };
90
+ }
@@ -0,0 +1,142 @@
1
+ import { type AccessToken, type PublicEnvironment } from "./base.js";
2
+ import type { components } from "./generated/realtime.js";
3
+ type Schemas = components["schemas"];
4
+ export type RealtimeCursor = Schemas["Cursor"];
5
+ export type StreamEvent = Schemas["Event"];
6
+ export type StreamEventType = StreamEvent["event_type"];
7
+ export type StreamEventOf<T extends StreamEventType> = StreamEvent extends infer E ? E extends {
8
+ readonly event_type: infer K;
9
+ } ? T extends K ? E : never : never : never;
10
+ export type StreamSnapshot = Schemas["Snapshot"];
11
+ export type StreamEventFrame = Schemas["EventFrame"];
12
+ /** What a stream follows: the account (optionally with every open RFQ), or one maker. The client manages the cursor. */
13
+ export type StreamSubscription = Omit<Schemas["AccountSubscription"], "cursor"> | Omit<Schemas["MakerSubscription"], "cursor">;
14
+ /** A server frame: `channel` names what `data` carries. */
15
+ export type StreamFrame = {
16
+ readonly channel: string;
17
+ readonly data?: unknown;
18
+ };
19
+ /**
20
+ * `live` means subscribed and caught up: the snapshot applied, or every
21
+ * replayed event applied (with publication freshness, at the first
22
+ * publication checkpoint, which the server sends only after its replay).
23
+ * Anything else means books and pending reads are stale.
24
+ */
25
+ export type StreamStatus = "idle" | "connecting" | "live" | "disconnected" | "closed";
26
+ /**
27
+ * Opens the socket. In a browser the credential travels as the
28
+ * `auth.<token>` subprotocol with the page's Origin; elsewhere (Node 22+, bots)
29
+ * as a bearer header, so `headers` carries Authorization and any extra headers.
30
+ */
31
+ export type WebSocketFactory = (url: string, protocols: string[], headers: Record<string, string>) => WebSocket;
32
+ export type StreamOptions = {
33
+ readonly environment?: PublicEnvironment;
34
+ /** The API origin; without `streamBaseUrl` the stream connects to it. */
35
+ readonly baseUrl?: string;
36
+ /** The WebSocket origin, such as `wss://hip4-ws.totalis.trade`. Defaults to `baseUrl`, else the environment's stream host. */
37
+ readonly streamBaseUrl?: string;
38
+ readonly accessToken: AccessToken;
39
+ /** `publication_freshness` (1 s heartbeats and publication checkpoints) defaults on for an account, off for a maker. */
40
+ readonly subscription: StreamSubscription;
41
+ /**
42
+ * Resume after this cursor, for example one a bot persisted from `event.cursor`:
43
+ * the stream replays the retained events after it, or resets to a snapshot.
44
+ */
45
+ readonly cursor?: RealtimeCursor;
46
+ readonly webSocket?: WebSocketFactory;
47
+ /** Extra handshake headers outside a browser, such as the Origin a browser session requires. */
48
+ readonly headers?: Record<string, string>;
49
+ readonly reconnect?: {
50
+ readonly baseDelayMs?: number;
51
+ readonly maxDelayMs?: number;
52
+ };
53
+ /** Reconnect after this long without a frame. Default 5000 ms with publication freshness, otherwise off. */
54
+ readonly silenceTimeoutMs?: number;
55
+ };
56
+ type Listeners = {
57
+ /** A new event, in order and deduplicated, with the frame that carried it. */
58
+ event: (event: StreamEvent, frame: StreamEventFrame) => void;
59
+ snapshot: (snapshot: StreamSnapshot) => void;
60
+ /** Local state built from earlier events is void; replace it from the snapshot that follows. */
61
+ reset: () => void;
62
+ status: (status: StreamStatus) => void;
63
+ /** Every parsed frame, including publication checkpoints, market refreshes and read invalidations. */
64
+ frame: (frame: StreamFrame) => void;
65
+ error: (error: unknown) => void;
66
+ };
67
+ /**
68
+ * The stream closed for good: the server's NEVER reason (for example
69
+ * SUBSCRIPTION_DENIED or STREAM_REPLACED), or `closed` after `close()`. It
70
+ * will not reconnect. `commandSent`: it cut off a follow of a command already
71
+ * sent, which may still execute.
72
+ */
73
+ export declare class StreamClosedError extends Error {
74
+ readonly reason: string;
75
+ readonly closeCode: number;
76
+ readonly commandSent: boolean;
77
+ readonly name = "StreamClosedError";
78
+ constructor(reason: string, closeCode: number, commandSent?: boolean);
79
+ }
80
+ /** The account and maker stream subprotocol. */
81
+ export declare const realtimeProtocol = "totalis.realtime.v2";
82
+ export declare const marketStreamProtocol = "totalis.market.v2";
83
+ export declare function realtimeUrl(environment: PublicEnvironment, baseUrl?: string, streamBaseUrl?: string): string;
84
+ /**
85
+ * The public market stream URL. `path` is `fallback_stream` from
86
+ * `GET /v1/market-data/config` (a path, `/v1/market-stream`), joined to the
87
+ * stream origin rather than the API origin.
88
+ */
89
+ export declare function marketStreamUrl(environment: PublicEnvironment, baseUrl?: string, streamBaseUrl?: string, path?: string): string;
90
+ export declare function realtimeCredentialProtocol(accessToken: string): string;
91
+ /**
92
+ * The authenticated realtime stream (`GET /v1/stream`) with snapshot, replay and
93
+ * reconnect. Delivery is at least once; this client orders and deduplicates by
94
+ * cursor and `event_id`, reconnects with jittered backoff from the last applied
95
+ * cursor, and follows each close reason's retry: BACKOFF resumes, REFRESH takes
96
+ * a fresh snapshot, NEVER stops. It works with the browser WebSocket and Node's.
97
+ */
98
+ export declare class TotalisStream {
99
+ private readonly options;
100
+ status: StreamStatus;
101
+ /** The last applied cursor. Persist it to resume after a restart. */
102
+ cursor: RealtimeCursor | undefined;
103
+ private clockOffsetMs;
104
+ private closedWith;
105
+ private socket;
106
+ private attempt;
107
+ private nextDelayMs;
108
+ private timer;
109
+ private silence;
110
+ private reauthorized;
111
+ /** A REPLAY resume with publication freshness: live waits for the first publication frame, which follows the replayed events. */
112
+ private replaying;
113
+ private readonly seen;
114
+ private readonly listeners;
115
+ constructor(options: StreamOptions);
116
+ on<K extends keyof Listeners>(type: K, listener: Listeners[K]): () => void;
117
+ start(): this;
118
+ close(): void;
119
+ /** Why the stream closed for good, as an error; `commandSent` marks a follow of a command already sent. */
120
+ closeError(commandSent?: boolean): StreamClosedError;
121
+ /** The venue clock in ms, from the last subscription response or publication checkpoint's `server_time`. */
122
+ serverNow(): number;
123
+ /** Resolves once the stream is live (starting it if needed); rejects if it closes for good. */
124
+ live(signal?: AbortSignal): Promise<void>;
125
+ /** The first event the predicate accepts. Rejects if the stream closes for good or the signal aborts. */
126
+ next<E extends StreamEvent>(predicate: (event: StreamEvent) => event is E, signal?: AbortSignal): Promise<E>;
127
+ next(predicate: (event: StreamEvent) => boolean, signal?: AbortSignal): Promise<StreamEvent>;
128
+ private until;
129
+ private emit;
130
+ private setStatus;
131
+ private reset;
132
+ private apply;
133
+ private freshness;
134
+ private connect;
135
+ private subscribe;
136
+ private onFrame;
137
+ private goLive;
138
+ private onClose;
139
+ private disconnected;
140
+ private watchSilence;
141
+ }
142
+ export {};
package/dist/stream.js ADDED
@@ -0,0 +1,331 @@
1
+ import { streamServerUrl } from "./base.js";
2
+ import { errorRegistry } from "./generated/errors.js";
3
+ import { backoffDelay } from "./request.js";
4
+ const inBrowser = () => typeof globalThis.document !== "undefined";
5
+ // Node's WebSocket (undici) and Bun accept `{ protocols, headers }` in place of protocols.
6
+ const openWebSocket = (url, protocols, headers) => inBrowser() ? new WebSocket(url, protocols) : new WebSocket(url, { protocols, headers });
7
+ /**
8
+ * The stream closed for good: the server's NEVER reason (for example
9
+ * SUBSCRIPTION_DENIED or STREAM_REPLACED), or `closed` after `close()`. It
10
+ * will not reconnect. `commandSent`: it cut off a follow of a command already
11
+ * sent, which may still execute.
12
+ */
13
+ export class StreamClosedError extends Error {
14
+ reason;
15
+ closeCode;
16
+ commandSent;
17
+ name = "StreamClosedError";
18
+ constructor(reason, closeCode, commandSent = false) {
19
+ super(`stream closed: ${reason || closeCode}`);
20
+ this.reason = reason;
21
+ this.closeCode = closeCode;
22
+ this.commandSent = commandSent;
23
+ }
24
+ }
25
+ /** The account and maker stream subprotocol. */
26
+ export const realtimeProtocol = "totalis.realtime.v2";
27
+ export const marketStreamProtocol = "totalis.market.v2";
28
+ export function realtimeUrl(environment, baseUrl, streamBaseUrl) {
29
+ return `${streamServerUrl(environment, baseUrl, streamBaseUrl)}/v1/stream`;
30
+ }
31
+ /**
32
+ * The public market stream URL. `path` is `fallback_stream` from
33
+ * `GET /v1/market-data/config` (a path, `/v1/market-stream`), joined to the
34
+ * stream origin rather than the API origin.
35
+ */
36
+ export function marketStreamUrl(environment, baseUrl, streamBaseUrl, path = "/v1/market-stream") {
37
+ return `${streamServerUrl(environment, baseUrl, streamBaseUrl)}${path}`;
38
+ }
39
+ export function realtimeCredentialProtocol(accessToken) {
40
+ const bytes = new TextEncoder().encode(accessToken);
41
+ let binary = "";
42
+ for (const byte of bytes)
43
+ binary += String.fromCharCode(byte);
44
+ return `auth.${btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "")}`;
45
+ }
46
+ const sameStream = (a, b) => a.stream_id === b.stream_id && a.generation_id === b.generation_id;
47
+ /**
48
+ * The authenticated realtime stream (`GET /v1/stream`) with snapshot, replay and
49
+ * reconnect. Delivery is at least once; this client orders and deduplicates by
50
+ * cursor and `event_id`, reconnects with jittered backoff from the last applied
51
+ * cursor, and follows each close reason's retry: BACKOFF resumes, REFRESH takes
52
+ * a fresh snapshot, NEVER stops. It works with the browser WebSocket and Node's.
53
+ */
54
+ export class TotalisStream {
55
+ options;
56
+ status = "idle";
57
+ /** The last applied cursor. Persist it to resume after a restart. */
58
+ cursor;
59
+ clockOffsetMs = 0;
60
+ closedWith;
61
+ socket;
62
+ attempt = 0;
63
+ nextDelayMs;
64
+ timer;
65
+ silence;
66
+ reauthorized = false;
67
+ /** A REPLAY resume with publication freshness: live waits for the first publication frame, which follows the replayed events. */
68
+ replaying = false;
69
+ seen = [];
70
+ listeners = {
71
+ event: new Set(), snapshot: new Set(), reset: new Set(), status: new Set(), frame: new Set(), error: new Set(),
72
+ };
73
+ constructor(options) {
74
+ this.options = options;
75
+ this.cursor = options.cursor;
76
+ }
77
+ on(type, listener) {
78
+ this.listeners[type].add(listener);
79
+ return () => this.listeners[type].delete(listener);
80
+ }
81
+ start() {
82
+ if (this.status === "idle")
83
+ void this.connect();
84
+ return this;
85
+ }
86
+ close() {
87
+ this.setStatus("closed");
88
+ clearTimeout(this.timer);
89
+ clearTimeout(this.silence);
90
+ const socket = this.socket;
91
+ this.socket = undefined;
92
+ socket?.close(1000);
93
+ }
94
+ /** Why the stream closed for good, as an error; `commandSent` marks a follow of a command already sent. */
95
+ closeError(commandSent = false) {
96
+ return new StreamClosedError(this.closedWith?.reason ?? "closed", this.closedWith?.code ?? 1000, commandSent);
97
+ }
98
+ /** The venue clock in ms, from the last subscription response or publication checkpoint's `server_time`. */
99
+ serverNow() {
100
+ return Date.now() + this.clockOffsetMs;
101
+ }
102
+ /** Resolves once the stream is live (starting it if needed); rejects if it closes for good. */
103
+ live(signal) {
104
+ this.start();
105
+ return this.until(() => this.status === "live" ? true : undefined, signal).then(() => undefined);
106
+ }
107
+ next(predicate, signal) {
108
+ this.start();
109
+ let found;
110
+ const off = this.on("event", (event) => {
111
+ if (found === undefined && predicate(event))
112
+ found = event;
113
+ });
114
+ return this.until(() => found, signal).finally(off);
115
+ }
116
+ until(check, signal) {
117
+ return new Promise((resolve, reject) => {
118
+ const offs = [];
119
+ const done = (settle) => {
120
+ for (const off of offs)
121
+ off();
122
+ signal?.removeEventListener("abort", abort);
123
+ settle();
124
+ };
125
+ const test = () => {
126
+ const value = check();
127
+ if (value !== undefined)
128
+ done(() => resolve(value));
129
+ else if (this.status === "closed")
130
+ done(() => reject(this.closeError()));
131
+ };
132
+ const abort = () => done(() => reject(signal?.reason));
133
+ if (signal?.aborted)
134
+ return reject(signal.reason);
135
+ signal?.addEventListener("abort", abort, { once: true });
136
+ offs.push(this.on("status", test), this.on("event", test));
137
+ test();
138
+ });
139
+ }
140
+ emit(type, ...args) {
141
+ for (const listener of [...this.listeners[type]]) {
142
+ try {
143
+ listener(...args);
144
+ }
145
+ catch (error) {
146
+ if (type !== "error")
147
+ this.emit("error", error);
148
+ }
149
+ }
150
+ }
151
+ setStatus(status) {
152
+ if (this.status === status || this.status === "closed")
153
+ return;
154
+ this.status = status;
155
+ this.emit("status", status);
156
+ }
157
+ reset() {
158
+ this.cursor = undefined;
159
+ this.seen.length = 0;
160
+ this.emit("reset");
161
+ }
162
+ apply(event, frame) {
163
+ const cursor = this.cursor;
164
+ if (cursor !== undefined && sameStream(cursor, event.cursor) && BigInt(event.cursor.sequence) <= BigInt(cursor.sequence))
165
+ return;
166
+ if (this.seen.includes(event.event_id))
167
+ return;
168
+ this.seen.push(event.event_id);
169
+ if (this.seen.length > 1024)
170
+ this.seen.shift();
171
+ this.cursor = event.cursor;
172
+ this.emit("event", event, frame);
173
+ }
174
+ freshness() {
175
+ const { type, publication_freshness = type === "account" } = this.options.subscription;
176
+ return publication_freshness;
177
+ }
178
+ async connect() {
179
+ this.replaying = false;
180
+ this.setStatus("connecting");
181
+ const { accessToken, environment = "production", baseUrl, streamBaseUrl } = this.options;
182
+ let socket;
183
+ try {
184
+ const token = typeof accessToken === "function" ? await accessToken() : accessToken;
185
+ if (this.status === "closed")
186
+ return;
187
+ const open = this.options.webSocket ?? openWebSocket;
188
+ const url = realtimeUrl(environment, baseUrl, streamBaseUrl);
189
+ socket = inBrowser()
190
+ ? open(url, [realtimeProtocol, realtimeCredentialProtocol(token)], {})
191
+ : open(url, [realtimeProtocol], { ...this.options.headers, Authorization: `Bearer ${token}` });
192
+ }
193
+ catch (error) {
194
+ this.emit("error", error);
195
+ this.disconnected();
196
+ return;
197
+ }
198
+ this.socket = socket;
199
+ socket.onopen = () => socket.send(JSON.stringify(this.subscribe()));
200
+ socket.onmessage = (message) => {
201
+ if (socket !== this.socket)
202
+ return;
203
+ let frame;
204
+ try {
205
+ frame = JSON.parse(String(message.data));
206
+ }
207
+ catch (error) {
208
+ this.emit("error", error);
209
+ return;
210
+ }
211
+ if (typeof frame !== "object" || frame === null || !("channel" in frame) || typeof frame.channel !== "string") {
212
+ this.emit("error", new TypeError("stream frame without a channel"));
213
+ return;
214
+ }
215
+ this.onFrame(frame);
216
+ };
217
+ socket.onclose = (event) => {
218
+ if (socket === this.socket)
219
+ this.onClose(event.code, event.reason);
220
+ };
221
+ this.watchSilence();
222
+ }
223
+ subscribe() {
224
+ const { publication_freshness = this.options.subscription.type === "account", ...subscription } = this.options.subscription;
225
+ return {
226
+ method: "subscribe",
227
+ subscription: { ...subscription, ...(publication_freshness ? { publication_freshness } : {}), ...(this.cursor === undefined ? {} : { cursor: this.cursor }) },
228
+ };
229
+ }
230
+ onFrame(raw) {
231
+ this.watchSilence();
232
+ this.emit("frame", raw);
233
+ // The frames this client acts on, by their `channel`; any other channel is only emitted above.
234
+ const frame = raw;
235
+ switch (frame.channel) {
236
+ case "publication":
237
+ if (frame.data !== null)
238
+ this.clockOffsetMs = Date.parse(frame.data.server_time) - Date.now();
239
+ if (this.replaying) {
240
+ this.replaying = false;
241
+ this.goLive();
242
+ }
243
+ break;
244
+ case "subscriptionResponse": {
245
+ const ready = frame.data;
246
+ this.clockOffsetMs = Date.parse(ready.server_time) - Date.now();
247
+ if (ready.resume === "REPLAY") {
248
+ this.cursor = ready.cursor;
249
+ // The replayed events follow this frame and nothing marks their end
250
+ // except the server's first publication checkpoint after them.
251
+ // Without publication freshness there is no such marker, so the
252
+ // stream is live once subscribed and replayed events may still follow.
253
+ if (this.freshness())
254
+ this.replaying = true;
255
+ else
256
+ this.goLive();
257
+ }
258
+ else if (ready.resume === "RESET" || this.cursor !== undefined) {
259
+ this.reset();
260
+ }
261
+ break;
262
+ }
263
+ case "snapshot": {
264
+ const snapshot = frame.data;
265
+ this.cursor = snapshot.cursor;
266
+ this.seen.length = 0;
267
+ this.emit("snapshot", snapshot);
268
+ this.goLive();
269
+ break;
270
+ }
271
+ case "event":
272
+ this.apply(frame.data, frame);
273
+ break;
274
+ case "reset":
275
+ this.reset();
276
+ break;
277
+ case "drain":
278
+ this.nextDelayMs = frame.data.retry_after_ms;
279
+ break;
280
+ }
281
+ }
282
+ goLive() {
283
+ this.attempt = 0;
284
+ this.reauthorized = false;
285
+ this.setStatus("live");
286
+ }
287
+ onClose(code, reason) {
288
+ this.socket = undefined;
289
+ clearTimeout(this.silence);
290
+ if (this.status === "closed")
291
+ return;
292
+ const retry = errorRegistry[reason]?.retry ?? "BACKOFF";
293
+ // A renewable session may retry a revoked credential once with a fresh token.
294
+ const renew = reason === "AUTHORIZATION_REVOKED" && typeof this.options.accessToken === "function" && !this.reauthorized;
295
+ if (retry === "NEVER" && !renew) {
296
+ this.closedWith = { reason, code };
297
+ this.close();
298
+ this.emit("error", this.closeError());
299
+ return;
300
+ }
301
+ if (renew)
302
+ this.reauthorized = true;
303
+ if (retry === "REFRESH" && this.cursor !== undefined)
304
+ this.reset();
305
+ this.disconnected();
306
+ }
307
+ disconnected() {
308
+ if (this.status === "closed")
309
+ return;
310
+ this.setStatus("disconnected");
311
+ const { baseDelayMs = 500, maxDelayMs = 30_000 } = this.options.reconnect ?? {};
312
+ // A drain's hint is the same for every socket it closes: wait at least that long, spread over as long again.
313
+ const delay = this.nextDelayMs === undefined ? backoffDelay(this.attempt, baseDelayMs, maxDelayMs) : this.nextDelayMs * (1 + Math.random());
314
+ this.nextDelayMs = undefined;
315
+ this.attempt++;
316
+ clearTimeout(this.timer);
317
+ this.timer = setTimeout(() => void this.connect(), delay);
318
+ }
319
+ watchSilence() {
320
+ const { silenceTimeoutMs = this.freshness() ? 5000 : undefined } = this.options;
321
+ clearTimeout(this.silence);
322
+ if (silenceTimeoutMs === undefined)
323
+ return;
324
+ this.silence = setTimeout(() => {
325
+ const socket = this.socket;
326
+ this.socket = undefined;
327
+ socket?.close(4000, "silent");
328
+ this.disconnected();
329
+ }, silenceTimeoutMs);
330
+ }
331
+ }