redweb-client 0.1.0 → 0.3.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.cts 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-D35g1hrL.cjs';
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-D35g1hrL.cjs';
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.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-D35g1hrL.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-D35g1hrL.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;
@@ -297,6 +336,9 @@ var RedwebClient = class {
297
336
  const requestId = options.requestId ?? this.createRequestId();
298
337
  const timeoutMs = options.timeoutMs ?? this.requestTimeoutMs;
299
338
  assertNonNegativeInteger(timeoutMs, "timeoutMs");
339
+ if (options.responseType !== void 0 && (typeof options.responseType !== "string" || !options.responseType || options.responseType.length > 256)) {
340
+ return Promise.reject(new TypeError("responseType must be a non-empty string of at most 256 characters."));
341
+ }
300
342
  if (options.signal?.aborted) return Promise.reject(new DOMException("The request was aborted.", "AbortError"));
301
343
  if (this.pending.has(requestId)) return Promise.reject(new Error(`A Redweb request with id "${requestId}" is already pending.`));
302
344
  return new Promise((resolve, reject) => {
@@ -304,7 +346,7 @@ var RedwebClient = class {
304
346
  this.finishPending(requestId);
305
347
  reject(new Error(`Redweb request timed out after ${timeoutMs}ms.`));
306
348
  }, timeoutMs);
307
- const pending = { resolve, reject, timer };
349
+ const pending = { resolve, reject, timer, responseType: options.responseType };
308
350
  if (options.signal) {
309
351
  pending.signal = options.signal;
310
352
  pending.abort = () => {
@@ -411,7 +453,7 @@ var RedwebClient = class {
411
453
  const requestId = message.requestId;
412
454
  if (typeof requestId === "string") {
413
455
  const pending = this.pending.get(requestId);
414
- if (pending) {
456
+ if (pending && (pending.responseType === void 0 || message.type === pending.responseType || message.type === "error" && "error" in message)) {
415
457
  this.finishPending(requestId);
416
458
  if (message.type === "error" && "error" in message) pending.reject(new RedwebProtocolError(message));
417
459
  else pending.resolve(message);
@@ -458,21 +500,25 @@ var RedwebClient = class {
458
500
  flushQueue() {
459
501
  while (this.socket?.readyState === OPEN && this.queue.length > 0) {
460
502
  const entry = this.queue.shift();
461
- if (entry !== void 0) this.socket.send(entry.data);
503
+ this.socket.send(entry.data);
462
504
  }
463
505
  }
464
506
  scheduleReconnect() {
465
- if (this.disposed || !this.reconnect.enabled || this.reconnectAttempts >= this.reconnect.maxAttempts) return;
507
+ if (this.disposed || this.manualClose || !this.reconnect.enabled || this.reconnectAttempts >= this.reconnect.maxAttempts) return;
508
+ const generation = this.generation;
466
509
  const baseDelay = Math.min(
467
510
  this.reconnect.maxDelayMs,
468
511
  this.reconnect.initialDelayMs * this.reconnect.factor ** this.reconnectAttempts
469
512
  );
470
513
  const spread = baseDelay * this.reconnect.jitter;
471
514
  const delay = Math.max(0, Math.round(baseDelay - spread + this.random() * spread * 2));
515
+ if (!this.current(generation)) return;
472
516
  this.reconnectAttempts += 1;
473
517
  this.transition("reconnecting");
518
+ if (!this.current(generation)) return;
474
519
  this.reconnectTimer = setTimeout(() => {
475
520
  this.reconnectTimer = void 0;
521
+ if (!this.current(generation)) return;
476
522
  void this.openSocket(true).catch(() => {
477
523
  });
478
524
  }, delay);