@totalis/hip4-client 0.73.0 → 0.75.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.
package/dist/stream.js CHANGED
@@ -63,12 +63,15 @@ export class TotalisStream {
63
63
  nextDelayMs;
64
64
  timer;
65
65
  silence;
66
+ connecting;
67
+ /** Counts connects, so one abandoned while its token resolved never opens a socket. */
68
+ generation = 0;
66
69
  reauthorized = false;
67
70
  /** A REPLAY resume with publication freshness: live waits for the first publication frame, which follows the replayed events. */
68
71
  replaying = false;
69
72
  seen = [];
70
73
  listeners = {
71
- event: new Set(), snapshot: new Set(), reset: new Set(), status: new Set(), frame: new Set(), error: new Set(),
74
+ event: new Set(), snapshot: new Set(), capital: new Set(), reset: new Set(), status: new Set(), frame: new Set(), error: new Set(),
72
75
  };
73
76
  constructor(options) {
74
77
  this.options = options;
@@ -87,6 +90,7 @@ export class TotalisStream {
87
90
  this.setStatus("closed");
88
91
  clearTimeout(this.timer);
89
92
  clearTimeout(this.silence);
93
+ clearTimeout(this.connecting);
90
94
  const socket = this.socket;
91
95
  this.socket = undefined;
92
96
  socket?.close(1000);
@@ -172,17 +176,20 @@ export class TotalisStream {
172
176
  this.emit("event", event, frame);
173
177
  }
174
178
  freshness() {
175
- const { type, publication_freshness = type === "account" } = this.options.subscription;
179
+ const { publication_freshness = true } = this.options.subscription;
176
180
  return publication_freshness;
177
181
  }
178
182
  async connect() {
179
183
  this.replaying = false;
180
184
  this.setStatus("connecting");
181
- const { accessToken, environment = "production", baseUrl, streamBaseUrl } = this.options;
185
+ const generation = ++this.generation;
186
+ const { accessToken, environment = "production", baseUrl, streamBaseUrl, connectTimeoutMs = 10_000 } = this.options;
187
+ clearTimeout(this.connecting);
188
+ this.connecting = setTimeout(() => this.abandon("unsubscribed", `stream not subscribed after ${connectTimeoutMs} ms`), connectTimeoutMs);
182
189
  let socket;
183
190
  try {
184
191
  const token = typeof accessToken === "function" ? await accessToken() : accessToken;
185
- if (this.status === "closed")
192
+ if (this.status === "closed" || generation !== this.generation)
186
193
  return;
187
194
  const open = this.options.webSocket ?? openWebSocket;
188
195
  const url = realtimeUrl(environment, baseUrl, streamBaseUrl);
@@ -191,6 +198,9 @@ export class TotalisStream {
191
198
  : open(url, [realtimeProtocol], { ...this.options.headers, Authorization: `Bearer ${token}` });
192
199
  }
193
200
  catch (error) {
201
+ if (generation !== this.generation)
202
+ return;
203
+ clearTimeout(this.connecting);
194
204
  this.emit("error", error);
195
205
  this.disconnected();
196
206
  return;
@@ -221,7 +231,7 @@ export class TotalisStream {
221
231
  this.watchSilence();
222
232
  }
223
233
  subscribe() {
224
- const { publication_freshness = this.options.subscription.type === "account", ...subscription } = this.options.subscription;
234
+ const { publication_freshness = true, ...subscription } = this.options.subscription;
225
235
  return {
226
236
  method: "subscribe",
227
237
  subscription: { ...subscription, ...(publication_freshness ? { publication_freshness } : {}), ...(this.cursor === undefined ? {} : { cursor: this.cursor }) },
@@ -242,6 +252,7 @@ export class TotalisStream {
242
252
  }
243
253
  break;
244
254
  case "subscriptionResponse": {
255
+ clearTimeout(this.connecting);
245
256
  const ready = frame.data;
246
257
  this.clockOffsetMs = Date.parse(ready.server_time) - Date.now();
247
258
  if (ready.resume === "REPLAY") {
@@ -271,6 +282,9 @@ export class TotalisStream {
271
282
  case "event":
272
283
  this.apply(frame.data, frame);
273
284
  break;
285
+ case "capital":
286
+ this.emit("capital", frame.data);
287
+ break;
274
288
  case "reset":
275
289
  this.reset();
276
290
  break;
@@ -287,6 +301,7 @@ export class TotalisStream {
287
301
  onClose(code, reason) {
288
302
  this.socket = undefined;
289
303
  clearTimeout(this.silence);
304
+ clearTimeout(this.connecting);
290
305
  if (this.status === "closed")
291
306
  return;
292
307
  const retry = errorRegistry[reason]?.retry ?? "BACKOFF";
@@ -321,11 +336,17 @@ export class TotalisStream {
321
336
  clearTimeout(this.silence);
322
337
  if (silenceTimeoutMs === undefined)
323
338
  return;
324
- this.silence = setTimeout(() => {
325
- const socket = this.socket;
326
- this.socket = undefined;
327
- socket?.close(4000, "silent");
328
- this.disconnected();
329
- }, silenceTimeoutMs);
339
+ this.silence = setTimeout(() => this.abandon("silent", `stream silent for ${silenceTimeoutMs} ms`), silenceTimeoutMs);
340
+ }
341
+ /** Gives up on the current connect or socket, reports why and reconnects. */
342
+ abandon(reason, message) {
343
+ this.generation++;
344
+ clearTimeout(this.silence);
345
+ clearTimeout(this.connecting);
346
+ const socket = this.socket;
347
+ this.socket = undefined;
348
+ socket?.close(4000, reason);
349
+ this.emit("error", new Error(message));
350
+ this.disconnected();
330
351
  }
331
352
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@totalis/hip4-client",
3
- "version": "0.73.0",
3
+ "version": "0.75.0",
4
4
  "description": "Totalis HIP-4 SDK: typed HTTP clients, signer-agnostic typed data, a retrying request layer, pagination, a realtime stream and taker/maker flows",
5
5
  "type": "module",
6
6
  "files": [