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/README.md +83 -4
- package/dist/client-Dq8oKJwh.d.cts +132 -0
- package/dist/client-Dq8oKJwh.d.ts +132 -0
- package/dist/index.cjs +54 -11
- package/dist/index.d.cts +3 -128
- package/dist/index.d.ts +3 -128
- package/dist/index.js +54 -11
- package/dist/live-html.cjs +1049 -0
- package/dist/live-html.d.cts +12 -0
- package/dist/live-html.d.ts +12 -0
- package/dist/live-html.js +1021 -0
- package/package.json +11 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,130 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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 {
|
|
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
|
-
|
|
188
|
-
|
|
188
|
+
let resolve;
|
|
189
|
+
let reject;
|
|
190
|
+
const promise = new Promise((yes, no) => {
|
|
191
|
+
resolve = yes;
|
|
192
|
+
reject = no;
|
|
189
193
|
});
|
|
190
|
-
|
|
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.
|
|
199
|
-
|
|
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
|
-
|
|
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);
|