redweb-client 0.1.0 → 0.2.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/index.d.ts CHANGED
@@ -1,130 +1,5 @@
1
- type ClientState = 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closing' | 'closed';
2
- interface ProtocolMetadata {
3
- requestId?: string;
4
- sequence?: number;
5
- }
6
- interface ProtocolEnvelope<T = unknown> extends ProtocolMetadata {
7
- v: string;
8
- type: string;
9
- payload: T;
10
- }
11
- interface ProtocolErrorEnvelope extends ProtocolMetadata {
12
- v: string;
13
- type: 'error';
14
- error: {
15
- code: string;
16
- message: string;
17
- };
18
- }
19
- interface LegacyErrorMessage extends ProtocolMetadata {
20
- error: string;
21
- type?: never;
22
- }
23
- type RedwebMessage<T = unknown> = ProtocolEnvelope<T> | ProtocolErrorEnvelope | LegacyErrorMessage | ({
24
- type: string;
25
- requestId?: string;
26
- sequence?: number;
27
- } & Record<string, unknown>);
28
- interface SocketEventLike {
29
- data?: unknown;
30
- code?: number;
31
- reason?: string;
32
- wasClean?: boolean;
33
- error?: unknown;
34
- }
35
- interface WebSocketLike {
36
- readonly readyState: number;
37
- binaryType?: string;
38
- send(data: string | ArrayBufferLike | ArrayBufferView | Blob): void;
39
- close(code?: number, reason?: string): void;
40
- addEventListener(type: 'open' | 'message' | 'error' | 'close', listener: (event: SocketEventLike) => void): void;
41
- removeEventListener(type: 'open' | 'message' | 'error' | 'close', listener: (event: SocketEventLike) => void): void;
42
- }
43
- interface ReconnectOptions {
44
- enabled?: boolean;
45
- maxAttempts?: number;
46
- initialDelayMs?: number;
47
- maxDelayMs?: number;
48
- factor?: number;
49
- jitter?: number;
50
- shouldReconnect?: (event: SocketEventLike) => boolean;
51
- }
52
- interface RedwebClientOptions {
53
- version?: string;
54
- reconnect?: ReconnectOptions;
55
- maxQueueSize?: number;
56
- requestTimeoutMs?: number;
57
- webSocketFactory?: (url: string) => WebSocketLike;
58
- createRequestId?: () => string;
59
- baseUrl?: string | URL;
60
- random?: () => number;
61
- }
62
- interface RequestOptions extends ProtocolMetadata {
63
- timeoutMs?: number;
64
- signal?: AbortSignal;
65
- }
66
- interface WaitOptions {
67
- timeoutMs?: number;
68
- signal?: AbortSignal;
69
- }
70
- type MessageListener<T = unknown> = (message: RedwebMessage<T>) => void;
71
- type BinaryListener = (data: ArrayBuffer | ArrayBufferView | Blob) => void;
72
- type ErrorListener = (error: Error) => void;
73
- type StateListener = (state: ClientState) => void;
74
- type CloseListener = (event: SocketEventLike) => void;
75
-
76
- declare class RedwebClient<IncomingEvents extends object = Record<string, unknown>, OutgoingEvents extends object = Record<string, unknown>> {
77
- readonly url: string;
78
- readonly version: string | undefined;
79
- state: ClientState;
80
- socket: WebSocketLike | undefined;
81
- private readonly factory;
82
- private readonly reconnect;
83
- private readonly maxQueueSize;
84
- private readonly requestTimeoutMs;
85
- private readonly createRequestId;
86
- private readonly random;
87
- private readonly listeners;
88
- private readonly anyListeners;
89
- private readonly binaryListeners;
90
- private readonly errorListeners;
91
- private readonly stateListeners;
92
- private readonly closeListeners;
93
- private readonly queue;
94
- private readonly pending;
95
- private connectPromise;
96
- private reconnectTimer;
97
- private reconnectAttempts;
98
- private manualClose;
99
- private disposed;
100
- private requestSequence;
101
- constructor(url: string, options?: RedwebClientOptions);
102
- connect(): Promise<void>;
103
- private openSocket;
104
- close(code?: number, reason?: string): void;
105
- dispose(): void;
106
- send<K extends keyof OutgoingEvents & string>(type: K, payload: OutgoingEvents[K], metadata?: ProtocolMetadata): void;
107
- sendRaw(data: string | ArrayBufferLike | ArrayBufferView | Blob): void;
108
- private transmit;
109
- request<TResponse = unknown, TPayload = unknown>(type: string, payload: TPayload, options?: RequestOptions): Promise<RedwebMessage<TResponse>>;
110
- waitFor<K extends keyof IncomingEvents & string>(type: K, options?: WaitOptions): Promise<RedwebMessage<IncomingEvents[K]>>;
111
- on<K extends keyof IncomingEvents & string>(type: K, listener: MessageListener<IncomingEvents[K]>): () => void;
112
- onAny(listener: MessageListener): () => void;
113
- onBinary(listener: BinaryListener): () => void;
114
- onError(listener: ErrorListener): () => void;
115
- onStateChange(listener: StateListener): () => void;
116
- onClose(listener: CloseListener): () => void;
117
- private subscribe;
118
- private connectionUrl;
119
- private transition;
120
- private handleIncoming;
121
- private finishPending;
122
- private rejectPending;
123
- private emitError;
124
- private notify;
125
- private flushQueue;
126
- private scheduleReconnect;
127
- }
1
+ import { P as ProtocolErrorEnvelope, a as ProtocolMetadata, R as RedwebMessage, b as ProtocolEnvelope } from './client-Dq8oKJwh.js';
2
+ export { B as BinaryListener, C as ClientState, c as CloseListener, E as ErrorListener, L as LegacyErrorMessage, M as MessageListener, d as ReconnectOptions, e as RedwebClient, f as RedwebClientOptions, g as RequestOptions, S as SocketEventLike, h as StateListener, W as WaitOptions, i as WebSocketLike } from './client-Dq8oKJwh.js';
128
3
 
129
4
  declare const ERROR_CODES: Readonly<{
130
5
  readonly INVALID_MESSAGE: "INVALID_MESSAGE";
@@ -146,4 +21,4 @@ declare class RedwebProtocolError extends Error {
146
21
  constructor(envelope: ProtocolErrorEnvelope);
147
22
  }
148
23
 
149
- export { type BinaryListener, type ClientState, type CloseListener, ERROR_CODES, type ErrorListener, type LegacyErrorMessage, type MessageListener, type ProtocolEnvelope, type ProtocolErrorEnvelope, type ProtocolMetadata, type ReconnectOptions, RedwebClient, type RedwebClientOptions, type RedwebMessage, RedwebProtocolError, type RequestOptions, type SocketEventLike, type StateListener, type WaitOptions, type WebSocketLike, createLegacyEnvelope, createProtocolEnvelope, parseMessage, resolveWebSocketUrl };
24
+ export { ERROR_CODES, ProtocolEnvelope, ProtocolErrorEnvelope, ProtocolMetadata, RedwebMessage, RedwebProtocolError, createLegacyEnvelope, createProtocolEnvelope, parseMessage, resolveWebSocketUrl };
package/dist/index.js CHANGED
@@ -146,6 +146,7 @@ var RedwebClient = class {
146
146
  manualClose = false;
147
147
  disposed = false;
148
148
  requestSequence = 0;
149
+ generation = 0;
149
150
  constructor(url, options = {}) {
150
151
  this.url = resolveWebSocketUrl(url, options.baseUrl);
151
152
  const queryVersion = new URL(this.url).searchParams.get("redwebVersion") ?? void 0;
@@ -184,27 +185,51 @@ var RedwebClient = class {
184
185
  clearTimeout(this.reconnectTimer);
185
186
  this.reconnectTimer = void 0;
186
187
  }
187
- this.connectPromise = this.openSocket(false).finally(() => {
188
- this.connectPromise = void 0;
188
+ let resolve;
189
+ let reject;
190
+ const promise = new Promise((yes, no) => {
191
+ resolve = yes;
192
+ reject = no;
189
193
  });
190
- return this.connectPromise;
194
+ this.connectPromise = promise;
195
+ const finish = () => this.retire(promise);
196
+ void this.openSocket(false).then(() => {
197
+ finish();
198
+ resolve();
199
+ }, (error) => {
200
+ finish();
201
+ reject(error);
202
+ });
203
+ return promise;
204
+ }
205
+ current(generation) {
206
+ return this.generation === generation && !this.manualClose && !this.disposed;
207
+ }
208
+ retire(promise) {
209
+ if (this.connectPromise === promise) this.connectPromise = void 0;
191
210
  }
192
211
  openSocket(reconnecting) {
212
+ const opening = this.connectPromise;
213
+ const generation = ++this.generation;
193
214
  this.transition(reconnecting ? "reconnecting" : "connecting");
215
+ if (!this.current(generation)) return Promise.reject(new Error("WebSocket connection attempt was cancelled."));
194
216
  let socket;
195
217
  try {
196
218
  socket = this.factory(this.connectionUrl());
197
219
  } catch (error) {
198
- this.transition("closed");
199
- if (reconnecting && !this.manualClose) this.scheduleReconnect();
220
+ if (this.current(generation)) {
221
+ this.retire(opening);
222
+ this.transition("closed");
223
+ if (reconnecting && this.current(generation)) this.scheduleReconnect();
224
+ }
200
225
  return Promise.reject(toError(error, "Unable to create WebSocket."));
201
226
  }
202
- this.socket = socket;
227
+ if (this.current(generation)) this.socket = socket;
203
228
  if ("binaryType" in socket) socket.binaryType = "arraybuffer";
204
229
  return new Promise((resolve, reject) => {
205
230
  let settled = false;
206
231
  const onOpen = () => {
207
- if (this.socket !== socket) {
232
+ if (this.socket !== socket || !this.current(generation)) {
208
233
  if (!settled) reject(new Error("WebSocket connection attempt was superseded."));
209
234
  socket.close(1e3, "Superseded");
210
235
  return;
@@ -212,11 +237,15 @@ var RedwebClient = class {
212
237
  settled = true;
213
238
  this.reconnectAttempts = 0;
214
239
  this.transition("open");
240
+ if (!this.current(generation)) {
241
+ reject(new Error("WebSocket connection attempt was cancelled."));
242
+ return;
243
+ }
215
244
  this.flushQueue();
216
245
  resolve();
217
246
  };
218
247
  const onMessage = (event) => {
219
- if (this.socket === socket) void this.handleIncoming(event.data);
248
+ if (this.socket === socket && this.current(generation)) void this.handleIncoming(event.data);
220
249
  };
221
250
  const onError = (event) => {
222
251
  if (this.socket !== socket) return;
@@ -238,18 +267,27 @@ var RedwebClient = class {
238
267
  reject(new Error("WebSocket closed before opening."));
239
268
  }
240
269
  this.rejectPending(new Error("WebSocket connection closed."));
270
+ const closedGeneration = this.generation;
271
+ this.retire(opening);
241
272
  this.transition("closed");
242
273
  this.notify(this.closeListeners, event);
243
- if (!this.manualClose && this.reconnect.shouldReconnect(event)) this.scheduleReconnect();
274
+ if (!this.manualClose && this.reconnect.shouldReconnect(event) && this.generation === closedGeneration) this.scheduleReconnect();
244
275
  };
245
276
  socket.addEventListener("open", onOpen);
246
277
  socket.addEventListener("message", onMessage);
247
278
  socket.addEventListener("error", onError);
248
279
  socket.addEventListener("close", onClose);
280
+ if (!this.current(generation)) {
281
+ settled = true;
282
+ reject(new Error("WebSocket connection attempt was cancelled."));
283
+ socket.close(1e3, "Cancelled");
284
+ }
249
285
  });
250
286
  }
251
287
  close(code = 1e3, reason = "Client closed") {
252
288
  this.manualClose = true;
289
+ this.generation++;
290
+ this.connectPromise = void 0;
253
291
  if (this.reconnectTimer) {
254
292
  clearTimeout(this.reconnectTimer);
255
293
  this.reconnectTimer = void 0;
@@ -284,6 +322,7 @@ var RedwebClient = class {
284
322
  this.transmit(data);
285
323
  }
286
324
  transmit(data, requestId) {
325
+ if (this.disposed) throw new Error("RedwebClient has been disposed.");
287
326
  if (this.socket?.readyState === OPEN) {
288
327
  this.socket.send(data);
289
328
  return;
@@ -458,21 +497,25 @@ var RedwebClient = class {
458
497
  flushQueue() {
459
498
  while (this.socket?.readyState === OPEN && this.queue.length > 0) {
460
499
  const entry = this.queue.shift();
461
- if (entry !== void 0) this.socket.send(entry.data);
500
+ this.socket.send(entry.data);
462
501
  }
463
502
  }
464
503
  scheduleReconnect() {
465
- if (this.disposed || !this.reconnect.enabled || this.reconnectAttempts >= this.reconnect.maxAttempts) return;
504
+ if (this.disposed || this.manualClose || !this.reconnect.enabled || this.reconnectAttempts >= this.reconnect.maxAttempts) return;
505
+ const generation = this.generation;
466
506
  const baseDelay = Math.min(
467
507
  this.reconnect.maxDelayMs,
468
508
  this.reconnect.initialDelayMs * this.reconnect.factor ** this.reconnectAttempts
469
509
  );
470
510
  const spread = baseDelay * this.reconnect.jitter;
471
511
  const delay = Math.max(0, Math.round(baseDelay - spread + this.random() * spread * 2));
512
+ if (!this.current(generation)) return;
472
513
  this.reconnectAttempts += 1;
473
514
  this.transition("reconnecting");
515
+ if (!this.current(generation)) return;
474
516
  this.reconnectTimer = setTimeout(() => {
475
517
  this.reconnectTimer = void 0;
518
+ if (!this.current(generation)) return;
476
519
  void this.openSocket(true).catch(() => {
477
520
  });
478
521
  }, delay);