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/README.md
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
# redweb-client
|
|
2
2
|
|
|
3
|
-
Browser-first WebSocket client for [Redweb](https://redweb.magnisolution.com/). It adds connection lifecycle, typed message subscriptions, request correlation, bounded queuing, opt-in reconnection, and
|
|
3
|
+
Browser-first WebSocket client for [Redweb](https://redweb.magnisolution.com/). It adds connection lifecycle, typed message subscriptions, request correlation, bounded queuing, opt-in reconnection, and versioned Redweb protocol envelopes without adding runtime dependencies.
|
|
4
|
+
|
|
5
|
+
## Unreleased lifecycle hardening
|
|
6
|
+
|
|
7
|
+
The development branch fixes connection attempts continuing after a synchronous state observer calls `close()` or `dispose()`. Connection attempts are identified across callback boundaries; a reentrant `connect()` shares an existing attempt, and an explicit replacement is not cleared by an older attempt finishing. Cancelled transport-factory results are closed after their lifecycle listeners are attached. These changes are not yet published in npm version 0.1.0.
|
|
8
|
+
|
|
9
|
+
Integration regressions use real WebSocket connections and timers for observer-driven cancellation, upgrade-refusal retries, queued delivery, cancellation without replay, and rejected sends after disposal. Unit tests use isolated transport fixtures and explicitly simulated stale scheduling; integration tests do not mock transports. The complete verification command and its coverage scope are described below.
|
|
10
|
+
|
|
11
|
+
## Unreleased Live HTML integration
|
|
12
|
+
|
|
13
|
+
The optional `redweb-client/live-html` entry owns reactive DOM updates, keyed
|
|
14
|
+
reconciliation, delegated actions/forms, feedback and connection status for pages
|
|
15
|
+
rendered by the matching Redweb development branch. The root import stays
|
|
16
|
+
socket-only. Redweb serves the optional self-contained module and automatically
|
|
17
|
+
calls `mountLivePage()`; ordinary applications need no additional browser glue.
|
|
18
|
+
|
|
19
|
+
For explicit lifecycle ownership in a browser bundle:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { mountLivePage } from 'redweb-client/live-html'
|
|
23
|
+
|
|
24
|
+
const page = mountLivePage() // Same owner if already mounted on this document.
|
|
25
|
+
// Later, when leaving the page:
|
|
26
|
+
page.dispose() // Closes transport and removes owned listeners/feedback.
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Mount only after Redweb's `__redweb_page` configuration and page DOM exist.
|
|
30
|
+
Calling it again after disposal creates a fresh owner. Importing the module alone
|
|
31
|
+
does not access the DOM or open a socket. A connection listener that calls mount
|
|
32
|
+
again shares the same owner; a disposed page cannot later reset a submitted draft.
|
|
33
|
+
|
|
34
|
+
For local development with sibling repositories, run `npm run build` and
|
|
35
|
+
`npm link --ignore-scripts` here, then `npm link redweb-client --no-save --ignore-scripts`
|
|
36
|
+
in Redweb. Rebuild here after source changes; no repacking is needed. The link is
|
|
37
|
+
not saved in either lockfile. Published 0.1.0 does not include this entry; client
|
|
38
|
+
publication and Redweb dependency/lockfile updates remain release prerequisites.
|
|
39
|
+
|
|
40
|
+
Redweb's native-browser gate checks the server-side counter, multi-user chat,
|
|
41
|
+
dashboard and forms using real HTTP/WebSockets. Its rendering coverage verifier
|
|
42
|
+
measures all bundled Live HTML modules separately from transport and currently
|
|
43
|
+
covers 426 statements, 262 branches, 64 functions and 351 lines (100% each).
|
|
44
|
+
This is emitted-frontend coverage, not complete original-source/client-package or
|
|
45
|
+
cross-browser certification. Separate V8 measurements are not substituted for
|
|
46
|
+
the complete original-source gate below; the full release gate is not approved.
|
|
47
|
+
|
|
48
|
+
From the linked Redweb repository, `npm run verify:client:source-coverage` runs
|
|
49
|
+
all 77 client tests both plain and instrumented, then native browser tests with
|
|
50
|
+
the same original-source coverage maps. Plain browser bundles must match this
|
|
51
|
+
client's `dist` output. It retains separate Node/browser results and currently
|
|
52
|
+
covers all 791 statements, 521 branches, 125 functions and 659 lines (100% each).
|
|
53
|
+
The same gate runs through the default `npm test` command. This metric does not
|
|
54
|
+
replace V8 coverage or independently count every optional-chain short circuit.
|
|
4
55
|
|
|
5
56
|
## Install
|
|
6
57
|
|
|
@@ -142,7 +193,7 @@ TypeScript improves application ergonomics; it does not validate arbitrary appli
|
|
|
142
193
|
|
|
143
194
|
- `connect()` is idempotent while connecting or open and resolves after the socket opens.
|
|
144
195
|
- `close(code?, reason?)` suppresses reconnection, rejects pending requests, and discards unsent queued data.
|
|
145
|
-
- `dispose()` closes once, clears subscriptions and queued messages, and
|
|
196
|
+
- `dispose()` closes once, clears subscriptions and queued messages, and prevents reconnection and new sends/requests. Sends throw and requests reject after disposal, even when outbound queue capacity is available.
|
|
146
197
|
- `send(type, payload, metadata?)` builds a legacy or protocol message.
|
|
147
198
|
- `sendRaw(data)` sends text or binary data without encoding it.
|
|
148
199
|
- Sends while disconnected throw by default. Set a finite `maxQueueSize` to queue a bounded number of messages until the next open. A queued request is removed if it times out, is aborted, or is rejected by `close()`; a command cannot execute later after its caller has been told it was cancelled.
|
|
@@ -183,7 +234,35 @@ Browser WebSockets cannot set arbitrary request headers. Use secure, same-site c
|
|
|
183
234
|
## Development and verification
|
|
184
235
|
|
|
185
236
|
```bash
|
|
186
|
-
npm
|
|
237
|
+
npm test
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`npm test` delegates to `npm run check`; there is one complete verification path.
|
|
241
|
+
It requires the matching sibling `../redweb` development checkout, with this
|
|
242
|
+
client linked into it using `npm link`. The link's canonical path is checked
|
|
243
|
+
**before** building, so a different installed client cannot silently be verified.
|
|
244
|
+
The command then builds, checks declarations through the package export map, and
|
|
245
|
+
delegates to Redweb's existing combined source/browser runner. It runs the complete
|
|
246
|
+
unit and mock-free HTTP/WebSocket test inventory plus native Chromium rendering
|
|
247
|
+
and transport tests. Chrome/Chromium must be installed.
|
|
248
|
+
|
|
249
|
+
After a build, `npm run test:source` runs just that full verification, with the
|
|
250
|
+
same checkout check. You can also run from the matching Redweb checkout:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
npm run verify:client:source-coverage
|
|
187
254
|
```
|
|
188
255
|
|
|
189
|
-
The
|
|
256
|
+
The combined gate shares the same original-source instrumentation across the complete
|
|
257
|
+
Node test inventory and native Chromium rendering/transport tests. Plain and
|
|
258
|
+
instrumented behavior must agree, and all tracked statements, branches, functions
|
|
259
|
+
and lines must reach 100%. This is not exhaustive accounting of compiler-generated
|
|
260
|
+
or optional-chain branches. Packed-pair verification and publication are separate
|
|
261
|
+
release steps; this checkout-aware command is development tooling, not a runtime
|
|
262
|
+
dependency on the Redweb repository.
|
|
263
|
+
|
|
264
|
+
The separate `npm run test:v8` command retains the original Node-only V8 coverage gate
|
|
265
|
+
and its unchanged thresholds. It currently fails because browser modules are not
|
|
266
|
+
executed in Node. Renaming that diagnostic does not fix or waive its failure:
|
|
267
|
+
the default command uses the explicitly documented combined original-source
|
|
268
|
+
denominator, including actual browser execution, rather than the Node-only report.
|
|
@@ -0,0 +1,132 @@
|
|
|
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
|
+
private generation;
|
|
102
|
+
constructor(url: string, options?: RedwebClientOptions);
|
|
103
|
+
connect(): Promise<void>;
|
|
104
|
+
private current;
|
|
105
|
+
private retire;
|
|
106
|
+
private openSocket;
|
|
107
|
+
close(code?: number, reason?: string): void;
|
|
108
|
+
dispose(): void;
|
|
109
|
+
send<K extends keyof OutgoingEvents & string>(type: K, payload: OutgoingEvents[K], metadata?: ProtocolMetadata): void;
|
|
110
|
+
sendRaw(data: string | ArrayBufferLike | ArrayBufferView | Blob): void;
|
|
111
|
+
private transmit;
|
|
112
|
+
request<TResponse = unknown, TPayload = unknown>(type: string, payload: TPayload, options?: RequestOptions): Promise<RedwebMessage<TResponse>>;
|
|
113
|
+
waitFor<K extends keyof IncomingEvents & string>(type: K, options?: WaitOptions): Promise<RedwebMessage<IncomingEvents[K]>>;
|
|
114
|
+
on<K extends keyof IncomingEvents & string>(type: K, listener: MessageListener<IncomingEvents[K]>): () => void;
|
|
115
|
+
onAny(listener: MessageListener): () => void;
|
|
116
|
+
onBinary(listener: BinaryListener): () => void;
|
|
117
|
+
onError(listener: ErrorListener): () => void;
|
|
118
|
+
onStateChange(listener: StateListener): () => void;
|
|
119
|
+
onClose(listener: CloseListener): () => void;
|
|
120
|
+
private subscribe;
|
|
121
|
+
private connectionUrl;
|
|
122
|
+
private transition;
|
|
123
|
+
private handleIncoming;
|
|
124
|
+
private finishPending;
|
|
125
|
+
private rejectPending;
|
|
126
|
+
private emitError;
|
|
127
|
+
private notify;
|
|
128
|
+
private flushQueue;
|
|
129
|
+
private scheduleReconnect;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export { type BinaryListener as B, type ClientState as C, type ErrorListener as E, type LegacyErrorMessage as L, type MessageListener as M, type ProtocolErrorEnvelope as P, type RedwebMessage as R, type SocketEventLike as S, type WaitOptions as W, type ProtocolMetadata as a, type ProtocolEnvelope as b, type CloseListener as c, type ReconnectOptions as d, RedwebClient as e, type RedwebClientOptions as f, type RequestOptions as g, type StateListener as h, type WebSocketLike as i };
|
|
@@ -0,0 +1,132 @@
|
|
|
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
|
+
private generation;
|
|
102
|
+
constructor(url: string, options?: RedwebClientOptions);
|
|
103
|
+
connect(): Promise<void>;
|
|
104
|
+
private current;
|
|
105
|
+
private retire;
|
|
106
|
+
private openSocket;
|
|
107
|
+
close(code?: number, reason?: string): void;
|
|
108
|
+
dispose(): void;
|
|
109
|
+
send<K extends keyof OutgoingEvents & string>(type: K, payload: OutgoingEvents[K], metadata?: ProtocolMetadata): void;
|
|
110
|
+
sendRaw(data: string | ArrayBufferLike | ArrayBufferView | Blob): void;
|
|
111
|
+
private transmit;
|
|
112
|
+
request<TResponse = unknown, TPayload = unknown>(type: string, payload: TPayload, options?: RequestOptions): Promise<RedwebMessage<TResponse>>;
|
|
113
|
+
waitFor<K extends keyof IncomingEvents & string>(type: K, options?: WaitOptions): Promise<RedwebMessage<IncomingEvents[K]>>;
|
|
114
|
+
on<K extends keyof IncomingEvents & string>(type: K, listener: MessageListener<IncomingEvents[K]>): () => void;
|
|
115
|
+
onAny(listener: MessageListener): () => void;
|
|
116
|
+
onBinary(listener: BinaryListener): () => void;
|
|
117
|
+
onError(listener: ErrorListener): () => void;
|
|
118
|
+
onStateChange(listener: StateListener): () => void;
|
|
119
|
+
onClose(listener: CloseListener): () => void;
|
|
120
|
+
private subscribe;
|
|
121
|
+
private connectionUrl;
|
|
122
|
+
private transition;
|
|
123
|
+
private handleIncoming;
|
|
124
|
+
private finishPending;
|
|
125
|
+
private rejectPending;
|
|
126
|
+
private emitError;
|
|
127
|
+
private notify;
|
|
128
|
+
private flushQueue;
|
|
129
|
+
private scheduleReconnect;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export { type BinaryListener as B, type ClientState as C, type ErrorListener as E, type LegacyErrorMessage as L, type MessageListener as M, type ProtocolErrorEnvelope as P, type RedwebMessage as R, type SocketEventLike as S, type WaitOptions as W, type ProtocolMetadata as a, type ProtocolEnvelope as b, type CloseListener as c, type ReconnectOptions as d, RedwebClient as e, type RedwebClientOptions as f, type RequestOptions as g, type StateListener as h, type WebSocketLike as i };
|
package/dist/index.cjs
CHANGED
|
@@ -178,6 +178,7 @@ var RedwebClient = class {
|
|
|
178
178
|
manualClose = false;
|
|
179
179
|
disposed = false;
|
|
180
180
|
requestSequence = 0;
|
|
181
|
+
generation = 0;
|
|
181
182
|
constructor(url, options = {}) {
|
|
182
183
|
this.url = resolveWebSocketUrl(url, options.baseUrl);
|
|
183
184
|
const queryVersion = new URL(this.url).searchParams.get("redwebVersion") ?? void 0;
|
|
@@ -216,27 +217,51 @@ var RedwebClient = class {
|
|
|
216
217
|
clearTimeout(this.reconnectTimer);
|
|
217
218
|
this.reconnectTimer = void 0;
|
|
218
219
|
}
|
|
219
|
-
|
|
220
|
-
|
|
220
|
+
let resolve;
|
|
221
|
+
let reject;
|
|
222
|
+
const promise = new Promise((yes, no) => {
|
|
223
|
+
resolve = yes;
|
|
224
|
+
reject = no;
|
|
221
225
|
});
|
|
222
|
-
|
|
226
|
+
this.connectPromise = promise;
|
|
227
|
+
const finish = () => this.retire(promise);
|
|
228
|
+
void this.openSocket(false).then(() => {
|
|
229
|
+
finish();
|
|
230
|
+
resolve();
|
|
231
|
+
}, (error) => {
|
|
232
|
+
finish();
|
|
233
|
+
reject(error);
|
|
234
|
+
});
|
|
235
|
+
return promise;
|
|
236
|
+
}
|
|
237
|
+
current(generation) {
|
|
238
|
+
return this.generation === generation && !this.manualClose && !this.disposed;
|
|
239
|
+
}
|
|
240
|
+
retire(promise) {
|
|
241
|
+
if (this.connectPromise === promise) this.connectPromise = void 0;
|
|
223
242
|
}
|
|
224
243
|
openSocket(reconnecting) {
|
|
244
|
+
const opening = this.connectPromise;
|
|
245
|
+
const generation = ++this.generation;
|
|
225
246
|
this.transition(reconnecting ? "reconnecting" : "connecting");
|
|
247
|
+
if (!this.current(generation)) return Promise.reject(new Error("WebSocket connection attempt was cancelled."));
|
|
226
248
|
let socket;
|
|
227
249
|
try {
|
|
228
250
|
socket = this.factory(this.connectionUrl());
|
|
229
251
|
} catch (error) {
|
|
230
|
-
this.
|
|
231
|
-
|
|
252
|
+
if (this.current(generation)) {
|
|
253
|
+
this.retire(opening);
|
|
254
|
+
this.transition("closed");
|
|
255
|
+
if (reconnecting && this.current(generation)) this.scheduleReconnect();
|
|
256
|
+
}
|
|
232
257
|
return Promise.reject(toError(error, "Unable to create WebSocket."));
|
|
233
258
|
}
|
|
234
|
-
this.socket = socket;
|
|
259
|
+
if (this.current(generation)) this.socket = socket;
|
|
235
260
|
if ("binaryType" in socket) socket.binaryType = "arraybuffer";
|
|
236
261
|
return new Promise((resolve, reject) => {
|
|
237
262
|
let settled = false;
|
|
238
263
|
const onOpen = () => {
|
|
239
|
-
if (this.socket !== socket) {
|
|
264
|
+
if (this.socket !== socket || !this.current(generation)) {
|
|
240
265
|
if (!settled) reject(new Error("WebSocket connection attempt was superseded."));
|
|
241
266
|
socket.close(1e3, "Superseded");
|
|
242
267
|
return;
|
|
@@ -244,11 +269,15 @@ var RedwebClient = class {
|
|
|
244
269
|
settled = true;
|
|
245
270
|
this.reconnectAttempts = 0;
|
|
246
271
|
this.transition("open");
|
|
272
|
+
if (!this.current(generation)) {
|
|
273
|
+
reject(new Error("WebSocket connection attempt was cancelled."));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
247
276
|
this.flushQueue();
|
|
248
277
|
resolve();
|
|
249
278
|
};
|
|
250
279
|
const onMessage = (event) => {
|
|
251
|
-
if (this.socket === socket) void this.handleIncoming(event.data);
|
|
280
|
+
if (this.socket === socket && this.current(generation)) void this.handleIncoming(event.data);
|
|
252
281
|
};
|
|
253
282
|
const onError = (event) => {
|
|
254
283
|
if (this.socket !== socket) return;
|
|
@@ -270,18 +299,27 @@ var RedwebClient = class {
|
|
|
270
299
|
reject(new Error("WebSocket closed before opening."));
|
|
271
300
|
}
|
|
272
301
|
this.rejectPending(new Error("WebSocket connection closed."));
|
|
302
|
+
const closedGeneration = this.generation;
|
|
303
|
+
this.retire(opening);
|
|
273
304
|
this.transition("closed");
|
|
274
305
|
this.notify(this.closeListeners, event);
|
|
275
|
-
if (!this.manualClose && this.reconnect.shouldReconnect(event)) this.scheduleReconnect();
|
|
306
|
+
if (!this.manualClose && this.reconnect.shouldReconnect(event) && this.generation === closedGeneration) this.scheduleReconnect();
|
|
276
307
|
};
|
|
277
308
|
socket.addEventListener("open", onOpen);
|
|
278
309
|
socket.addEventListener("message", onMessage);
|
|
279
310
|
socket.addEventListener("error", onError);
|
|
280
311
|
socket.addEventListener("close", onClose);
|
|
312
|
+
if (!this.current(generation)) {
|
|
313
|
+
settled = true;
|
|
314
|
+
reject(new Error("WebSocket connection attempt was cancelled."));
|
|
315
|
+
socket.close(1e3, "Cancelled");
|
|
316
|
+
}
|
|
281
317
|
});
|
|
282
318
|
}
|
|
283
319
|
close(code = 1e3, reason = "Client closed") {
|
|
284
320
|
this.manualClose = true;
|
|
321
|
+
this.generation++;
|
|
322
|
+
this.connectPromise = void 0;
|
|
285
323
|
if (this.reconnectTimer) {
|
|
286
324
|
clearTimeout(this.reconnectTimer);
|
|
287
325
|
this.reconnectTimer = void 0;
|
|
@@ -316,6 +354,7 @@ var RedwebClient = class {
|
|
|
316
354
|
this.transmit(data);
|
|
317
355
|
}
|
|
318
356
|
transmit(data, requestId) {
|
|
357
|
+
if (this.disposed) throw new Error("RedwebClient has been disposed.");
|
|
319
358
|
if (this.socket?.readyState === OPEN) {
|
|
320
359
|
this.socket.send(data);
|
|
321
360
|
return;
|
|
@@ -490,21 +529,25 @@ var RedwebClient = class {
|
|
|
490
529
|
flushQueue() {
|
|
491
530
|
while (this.socket?.readyState === OPEN && this.queue.length > 0) {
|
|
492
531
|
const entry = this.queue.shift();
|
|
493
|
-
|
|
532
|
+
this.socket.send(entry.data);
|
|
494
533
|
}
|
|
495
534
|
}
|
|
496
535
|
scheduleReconnect() {
|
|
497
|
-
if (this.disposed || !this.reconnect.enabled || this.reconnectAttempts >= this.reconnect.maxAttempts) return;
|
|
536
|
+
if (this.disposed || this.manualClose || !this.reconnect.enabled || this.reconnectAttempts >= this.reconnect.maxAttempts) return;
|
|
537
|
+
const generation = this.generation;
|
|
498
538
|
const baseDelay = Math.min(
|
|
499
539
|
this.reconnect.maxDelayMs,
|
|
500
540
|
this.reconnect.initialDelayMs * this.reconnect.factor ** this.reconnectAttempts
|
|
501
541
|
);
|
|
502
542
|
const spread = baseDelay * this.reconnect.jitter;
|
|
503
543
|
const delay = Math.max(0, Math.round(baseDelay - spread + this.random() * spread * 2));
|
|
544
|
+
if (!this.current(generation)) return;
|
|
504
545
|
this.reconnectAttempts += 1;
|
|
505
546
|
this.transition("reconnecting");
|
|
547
|
+
if (!this.current(generation)) return;
|
|
506
548
|
this.reconnectTimer = setTimeout(() => {
|
|
507
549
|
this.reconnectTimer = void 0;
|
|
550
|
+
if (!this.current(generation)) return;
|
|
508
551
|
void this.openSocket(true).catch(() => {
|
|
509
552
|
});
|
|
510
553
|
}, delay);
|
package/dist/index.d.cts
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.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-Dq8oKJwh.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 {
|
|
24
|
+
export { ERROR_CODES, ProtocolEnvelope, ProtocolErrorEnvelope, ProtocolMetadata, RedwebMessage, RedwebProtocolError, createLegacyEnvelope, createProtocolEnvelope, parseMessage, resolveWebSocketUrl };
|