@replit/river 0.9.3 → 0.10.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/__tests__/bandwidth.bench.js +11 -11
- package/dist/__tests__/cleanup.test.d.ts +2 -0
- package/dist/__tests__/cleanup.test.d.ts.map +1 -0
- package/dist/__tests__/{invariants.test.js → cleanup.test.js} +47 -20
- package/dist/__tests__/disconnects.test.d.ts +2 -0
- package/dist/__tests__/disconnects.test.d.ts.map +1 -0
- package/dist/__tests__/disconnects.test.js +163 -0
- package/dist/__tests__/e2e.test.js +33 -32
- package/dist/__tests__/fixtures/cleanup.d.ts +2 -2
- package/dist/__tests__/fixtures/cleanup.d.ts.map +1 -1
- package/dist/__tests__/fixtures/cleanup.js +6 -9
- package/dist/__tests__/fixtures/services.d.ts +36 -36
- package/dist/__tests__/fixtures/services.d.ts.map +1 -1
- package/dist/__tests__/fixtures/services.js +36 -53
- package/dist/__tests__/handler.test.js +6 -7
- package/dist/__tests__/typescript-stress.test.d.ts +149 -149
- package/dist/__tests__/typescript-stress.test.d.ts.map +1 -1
- package/dist/__tests__/typescript-stress.test.js +14 -14
- package/dist/router/builder.d.ts +6 -7
- package/dist/router/builder.d.ts.map +1 -1
- package/dist/router/client.d.ts +7 -3
- package/dist/router/client.d.ts.map +1 -1
- package/dist/router/client.js +204 -106
- package/dist/router/defs.d.ts +16 -0
- package/dist/router/defs.d.ts.map +1 -0
- package/dist/router/defs.js +11 -0
- package/dist/router/index.d.ts +2 -0
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +1 -0
- package/dist/router/result.d.ts +2 -1
- package/dist/router/result.d.ts.map +1 -1
- package/dist/router/result.js +5 -1
- package/dist/router/server.d.ts +5 -5
- package/dist/router/server.d.ts.map +1 -1
- package/dist/router/server.js +125 -82
- package/dist/transport/impls/stdio/stdio.test.js +1 -2
- package/dist/transport/impls/ws/client.d.ts +1 -4
- package/dist/transport/impls/ws/client.d.ts.map +1 -1
- package/dist/transport/impls/ws/client.js +5 -6
- package/dist/transport/impls/ws/server.d.ts +3 -0
- package/dist/transport/impls/ws/server.d.ts.map +1 -1
- package/dist/transport/impls/ws/server.js +28 -23
- package/dist/transport/impls/ws/ws.test.js +84 -16
- package/dist/transport/index.d.ts +0 -9
- package/dist/transport/index.d.ts.map +1 -1
- package/dist/transport/index.js +0 -21
- package/dist/transport/message.d.ts +3 -4
- package/dist/transport/message.d.ts.map +1 -1
- package/dist/util/testHelpers.d.ts +20 -97
- package/dist/util/testHelpers.d.ts.map +1 -1
- package/dist/util/testHelpers.js +94 -249
- package/package.json +13 -12
- package/dist/__tests__/invariants.test.d.ts +0 -2
- package/dist/__tests__/invariants.test.d.ts.map +0 -1
package/dist/router/server.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Static } from '@sinclair/typebox';
|
|
2
2
|
import { Connection, Transport } from '../transport/transport';
|
|
3
|
-
import { AnyProcedure,
|
|
3
|
+
import { AnyProcedure, PayloadType } from './builder';
|
|
4
4
|
import type { Pushable } from 'it-pushable';
|
|
5
|
-
import { TransportMessage } from '../transport/message';
|
|
6
5
|
import { ServiceContext } from './context';
|
|
7
6
|
import { Result, RiverError } from './result';
|
|
7
|
+
import { ServiceDefs } from './defs';
|
|
8
8
|
/**
|
|
9
9
|
* Represents a server with a set of services. Use {@link createServer} to create it.
|
|
10
10
|
* @template Services - The type of services provided by the server.
|
|
@@ -19,8 +19,8 @@ interface ProcStream {
|
|
|
19
19
|
serviceName: string;
|
|
20
20
|
procedureName: string;
|
|
21
21
|
procedure: AnyProcedure;
|
|
22
|
-
incoming: Pushable<
|
|
23
|
-
outgoing: Pushable<
|
|
22
|
+
incoming: Pushable<PayloadType>;
|
|
23
|
+
outgoing: Pushable<Result<Static<PayloadType>, Static<RiverError>>>;
|
|
24
24
|
promises: {
|
|
25
25
|
outputHandler: Promise<unknown>;
|
|
26
26
|
inputHandler: Promise<unknown>;
|
|
@@ -34,6 +34,6 @@ interface ProcStream {
|
|
|
34
34
|
* @param extendedContext - An optional object containing additional context to be passed to all services.
|
|
35
35
|
* @returns A promise that resolves to a server instance with the registered services.
|
|
36
36
|
*/
|
|
37
|
-
export declare function createServer<Services extends
|
|
37
|
+
export declare function createServer<Services extends ServiceDefs>(transport: Transport<Connection>, services: Services, extendedContext?: Omit<ServiceContext, 'state'>): Server<Services>;
|
|
38
38
|
export {};
|
|
39
39
|
//# sourceMappingURL=server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../router/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../router/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAc,WAAW,EAAE,MAAM,WAAW,CAAC;AAElE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAU5C,OAAO,EAAE,cAAc,EAA2B,MAAM,WAAW,CAAC;AAGpE,OAAO,EAEL,MAAM,EACN,UAAU,EAGX,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAErC;;;GAGG;AACH,MAAM,WAAW,MAAM,CAAC,QAAQ;IAC9B,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,UAAU,UAAU;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,YAAY,CAAC;IACxB,QAAQ,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;IAChC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IACpE,QAAQ,EAAE;QACR,aAAa,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;QAChC,YAAY,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;KAChC,CAAC;CACH;AAsUD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,QAAQ,SAAS,WAAW,EACvD,SAAS,EAAE,SAAS,CAAC,UAAU,CAAC,EAChC,QAAQ,EAAE,QAAQ,EAClB,eAAe,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,GAC9C,MAAM,CAAC,QAAQ,CAAC,CAElB"}
|
package/dist/router/server.js
CHANGED
|
@@ -3,104 +3,109 @@ import { ControlMessagePayloadSchema, isStreamClose, isStreamOpen, reply, closeS
|
|
|
3
3
|
import { log } from '../logging';
|
|
4
4
|
import { Value } from '@sinclair/typebox/value';
|
|
5
5
|
import { Err, UNCAUGHT_ERROR, } from './result';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* @param services - An object containing all the services to be registered on the server.
|
|
11
|
-
* @param extendedContext - An optional object containing additional context to be passed to all services.
|
|
12
|
-
* @returns A promise that resolves to a server instance with the registered services.
|
|
13
|
-
*/
|
|
14
|
-
export async function createServer(transport, services, extendedContext) {
|
|
15
|
-
const contextMap = new Map();
|
|
6
|
+
class RiverServer {
|
|
7
|
+
transport;
|
|
8
|
+
services;
|
|
9
|
+
contextMap;
|
|
16
10
|
// map of streamId to ProcStream
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
streamMap;
|
|
12
|
+
// map of client to their open streams by streamId
|
|
13
|
+
clientStreams;
|
|
14
|
+
constructor(transport, services, extendedContext) {
|
|
15
|
+
this.transport = transport;
|
|
16
|
+
this.services = services;
|
|
17
|
+
this.contextMap = new Map();
|
|
18
|
+
for (const service of Object.values(services)) {
|
|
19
|
+
this.contextMap.set(service, {
|
|
20
|
+
...extendedContext,
|
|
21
|
+
state: service.state,
|
|
22
|
+
});
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
streamMap.delete(id);
|
|
24
|
+
this.streamMap = new Map();
|
|
25
|
+
this.clientStreams = new Map();
|
|
26
|
+
this.transport.addEventListener('message', this.handler);
|
|
27
|
+
this.transport.addEventListener('connectionStatus', this.onDisconnect);
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (!context) {
|
|
32
|
-
const err = `${transport.clientId} -- no context found for ${service.name}`;
|
|
33
|
-
log?.error(err);
|
|
34
|
-
throw new Error(err);
|
|
35
|
-
}
|
|
36
|
-
return context;
|
|
37
|
-
}
|
|
38
|
-
// populate the context map
|
|
39
|
-
for (const service of Object.values(services)) {
|
|
40
|
-
contextMap.set(service, { ...extendedContext, state: service.state });
|
|
29
|
+
get streams() {
|
|
30
|
+
return this.streamMap;
|
|
41
31
|
}
|
|
42
|
-
|
|
43
|
-
if (message.to !== transport.clientId) {
|
|
44
|
-
log?.info(`${transport.clientId} -- got msg with destination that isn't the server, ignoring`);
|
|
32
|
+
handler = async (message) => {
|
|
33
|
+
if (message.to !== this.transport.clientId) {
|
|
34
|
+
log?.info(`${this.transport.clientId} -- got msg with destination that isn't the server, ignoring`);
|
|
45
35
|
return;
|
|
46
36
|
}
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
if
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
else if (!Value.Check(ControlMessagePayloadSchema, message.payload)) {
|
|
55
|
-
log?.error(`${transport.clientId} -- procedure ${procStream.serviceName}.${procStream.procedureName} received invalid payload: ${JSON.stringify(message.payload)}`);
|
|
56
|
-
}
|
|
57
|
-
if (isStreamClose(message.controlFlags)) {
|
|
58
|
-
await cleanupStream(streamIdx);
|
|
59
|
-
}
|
|
37
|
+
let procStream = this.streamMap.get(message.streamId);
|
|
38
|
+
const isInitMessage = !procStream;
|
|
39
|
+
// create a proc stream if it doesnt exist
|
|
40
|
+
procStream ||= this.createNewProcStream(message);
|
|
41
|
+
if (!procStream) {
|
|
42
|
+
// if we fail to create a proc stream, just abort
|
|
60
43
|
return;
|
|
61
44
|
}
|
|
45
|
+
await this.pushToStream(procStream, message, isInitMessage);
|
|
46
|
+
};
|
|
47
|
+
// cleanup streams on unexpected disconnections
|
|
48
|
+
onDisconnect = async (evt) => {
|
|
49
|
+
if (evt.status !== 'disconnect') {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const disconnectedClientId = evt.conn.connectedTo;
|
|
53
|
+
log?.info(`${this.transport.clientId} -- got unexpected disconnect from ${disconnectedClientId}, cleaning up streams`);
|
|
54
|
+
const streamsFromThisClient = this.clientStreams.get(disconnectedClientId);
|
|
55
|
+
if (!streamsFromThisClient) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
await Promise.all(Array.from(streamsFromThisClient).map(this.cleanupStream));
|
|
59
|
+
this.clientStreams.delete(disconnectedClientId);
|
|
60
|
+
};
|
|
61
|
+
async close() {
|
|
62
|
+
this.transport.removeEventListener('message', this.handler);
|
|
63
|
+
this.transport.removeEventListener('connectionStatus', this.onDisconnect);
|
|
64
|
+
await Promise.all([...this.streamMap.keys()].map(this.cleanupStream));
|
|
65
|
+
}
|
|
66
|
+
createNewProcStream(message) {
|
|
62
67
|
if (!isStreamOpen(message.controlFlags)) {
|
|
63
|
-
log?.warn(`${transport.clientId} -- couldn't find a matching procedure stream for ${message.serviceName}.${message.procedureName}:${message.streamId}`);
|
|
68
|
+
log?.warn(`${this.transport.clientId} -- couldn't find a matching procedure stream for ${message.serviceName}.${message.procedureName}:${message.streamId}`);
|
|
64
69
|
return;
|
|
65
70
|
}
|
|
66
|
-
if (!message.serviceName || !(message.serviceName in services)) {
|
|
67
|
-
log?.warn(`${transport.clientId} -- couldn't find service ${message.serviceName}`);
|
|
71
|
+
if (!message.serviceName || !(message.serviceName in this.services)) {
|
|
72
|
+
log?.warn(`${this.transport.clientId} -- couldn't find service ${message.serviceName}`);
|
|
68
73
|
return;
|
|
69
74
|
}
|
|
70
|
-
const service = services[message.serviceName];
|
|
71
|
-
const serviceContext = getContext(service);
|
|
75
|
+
const service = this.services[message.serviceName];
|
|
76
|
+
const serviceContext = this.getContext(service);
|
|
72
77
|
if (!message.procedureName ||
|
|
73
78
|
!(message.procedureName in service.procedures)) {
|
|
74
|
-
log?.warn(`${transport.clientId} -- couldn't find a matching procedure for ${message.serviceName}.${message.procedureName}`);
|
|
79
|
+
log?.warn(`${this.transport.clientId} -- couldn't find a matching procedure for ${message.serviceName}.${message.procedureName}`);
|
|
75
80
|
return;
|
|
76
81
|
}
|
|
77
82
|
const procedure = service.procedures[message.procedureName];
|
|
78
|
-
const procHasInitMessage = 'init' in procedure;
|
|
79
83
|
const incoming = pushable({ objectMode: true });
|
|
80
84
|
const outgoing = pushable({ objectMode: true });
|
|
81
85
|
const outputHandler =
|
|
82
86
|
// sending outgoing messages back to client
|
|
83
87
|
(async () => {
|
|
84
88
|
for await (const response of outgoing) {
|
|
85
|
-
transport.send(response);
|
|
89
|
+
this.transport.send(reply(message, response));
|
|
86
90
|
}
|
|
87
91
|
// we ended, send a close bit back to the client
|
|
88
92
|
// only subscriptions and streams have streams the
|
|
89
93
|
// handler can close
|
|
90
94
|
if (procedure.type === 'subscription' || procedure.type === 'stream') {
|
|
91
|
-
transport.send(closeStream(transport.clientId, message.from, message.streamId));
|
|
95
|
+
this.transport.send(closeStream(this.transport.clientId, message.from, message.streamId));
|
|
92
96
|
}
|
|
93
97
|
})();
|
|
94
|
-
|
|
98
|
+
const errorHandler = (err) => {
|
|
95
99
|
const errorMsg = err instanceof Error ? err.message : `[coerced to error] ${err}`;
|
|
96
|
-
log?.error(`${transport.clientId} -- procedure ${message.serviceName}.${message.procedureName}:${message.streamId} threw an error: ${errorMsg}`);
|
|
97
|
-
outgoing.push(
|
|
100
|
+
log?.error(`${this.transport.clientId} -- procedure ${message.serviceName}.${message.procedureName}:${message.streamId} threw an error: ${errorMsg}`);
|
|
101
|
+
outgoing.push(Err({
|
|
98
102
|
code: UNCAUGHT_ERROR,
|
|
99
103
|
message: errorMsg,
|
|
100
|
-
}))
|
|
101
|
-
}
|
|
104
|
+
}));
|
|
105
|
+
};
|
|
102
106
|
// pump incoming message stream -> handler -> outgoing message stream
|
|
103
107
|
let inputHandler;
|
|
108
|
+
const procHasInitMessage = 'init' in procedure;
|
|
104
109
|
if (procedure.type === 'stream') {
|
|
105
110
|
if (procHasInitMessage) {
|
|
106
111
|
inputHandler = (async () => {
|
|
@@ -179,10 +184,10 @@ export async function createServer(transport, services, extendedContext) {
|
|
|
179
184
|
else {
|
|
180
185
|
// procedure is inferred to be never here as this is not a valid procedure type
|
|
181
186
|
// we cast just to log
|
|
182
|
-
log?.warn(`${transport.clientId} -- got request for invalid procedure type ${procedure.type} at ${message.serviceName}.${message.procedureName}`);
|
|
187
|
+
log?.warn(`${this.transport.clientId} -- got request for invalid procedure type ${procedure.type} at ${message.serviceName}.${message.procedureName}`);
|
|
183
188
|
return;
|
|
184
189
|
}
|
|
185
|
-
|
|
190
|
+
const procStream = {
|
|
186
191
|
id: message.streamId,
|
|
187
192
|
incoming,
|
|
188
193
|
outgoing,
|
|
@@ -190,28 +195,66 @@ export async function createServer(transport, services, extendedContext) {
|
|
|
190
195
|
procedureName: message.procedureName,
|
|
191
196
|
procedure,
|
|
192
197
|
promises: { inputHandler, outputHandler },
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
+
};
|
|
199
|
+
this.streamMap.set(message.streamId, procStream);
|
|
200
|
+
// add this stream to ones from that client so we can clean it up in the case of a disconnect without close
|
|
201
|
+
const streamsFromThisClient = this.clientStreams.get(message.from) ?? new Set();
|
|
202
|
+
streamsFromThisClient.add(message.streamId);
|
|
203
|
+
this.clientStreams.set(message.from, streamsFromThisClient);
|
|
204
|
+
return procStream;
|
|
205
|
+
}
|
|
206
|
+
async pushToStream(procStream, message, isInit) {
|
|
207
|
+
const procedure = procStream.procedure;
|
|
208
|
+
const procHasInitMessage = 'init' in procedure;
|
|
209
|
+
if ((isInit &&
|
|
210
|
+
procHasInitMessage &&
|
|
211
|
+
Value.Check(procedure.init, message.payload)) ||
|
|
212
|
+
Value.Check(procedure.input, message.payload)) {
|
|
213
|
+
procStream.incoming.push(message.payload);
|
|
198
214
|
}
|
|
199
215
|
else if (!Value.Check(ControlMessagePayloadSchema, message.payload)) {
|
|
200
|
-
log?.error(`${transport.clientId} -- procedure ${
|
|
216
|
+
log?.error(`${this.transport.clientId} -- procedure ${procStream.serviceName}.${procStream.procedureName} received invalid payload: ${JSON.stringify(message.payload)}`);
|
|
201
217
|
}
|
|
202
218
|
if (isStreamClose(message.controlFlags)) {
|
|
203
|
-
await cleanupStream(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
async close() {
|
|
211
|
-
transport.removeEventListener('message', handler);
|
|
212
|
-
for (const streamIdx of streamMap.keys()) {
|
|
213
|
-
await cleanupStream(streamIdx);
|
|
219
|
+
await this.cleanupStream(message.streamId);
|
|
220
|
+
const streamsFromThisClient = this.clientStreams.get(message.from);
|
|
221
|
+
if (streamsFromThisClient) {
|
|
222
|
+
streamsFromThisClient.delete(message.streamId);
|
|
223
|
+
if (streamsFromThisClient.size === 0) {
|
|
224
|
+
this.clientStreams.delete(message.from);
|
|
225
|
+
}
|
|
214
226
|
}
|
|
215
|
-
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
getContext(service) {
|
|
230
|
+
const context = this.contextMap.get(service);
|
|
231
|
+
if (!context) {
|
|
232
|
+
const err = `${this.transport.clientId} -- no context found for ${service.name}`;
|
|
233
|
+
log?.error(err);
|
|
234
|
+
throw new Error(err);
|
|
235
|
+
}
|
|
236
|
+
return context;
|
|
237
|
+
}
|
|
238
|
+
cleanupStream = async (id) => {
|
|
239
|
+
const stream = this.streamMap.get(id);
|
|
240
|
+
if (!stream) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
stream.incoming.end();
|
|
244
|
+
await stream.promises.inputHandler;
|
|
245
|
+
stream.outgoing.end();
|
|
246
|
+
await stream.promises.outputHandler;
|
|
247
|
+
this.streamMap.delete(id);
|
|
216
248
|
};
|
|
217
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* Creates a server instance that listens for incoming messages from a transport and routes them to the appropriate service and procedure.
|
|
252
|
+
* The server tracks the state of each service along with open streams and the extended context object.
|
|
253
|
+
* @param transport - The transport to listen to.
|
|
254
|
+
* @param services - An object containing all the services to be registered on the server.
|
|
255
|
+
* @param extendedContext - An optional object containing additional context to be passed to all services.
|
|
256
|
+
* @returns A promise that resolves to a server instance with the registered services.
|
|
257
|
+
*/
|
|
258
|
+
export function createServer(transport, services, extendedContext) {
|
|
259
|
+
return new RiverServer(transport, services, extendedContext);
|
|
260
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { describe, test, expect } from 'vitest';
|
|
2
2
|
import stream from 'node:stream';
|
|
3
3
|
import { StdioTransport } from './stdio';
|
|
4
|
-
import { waitForMessage } from '
|
|
5
|
-
import { payloadToTransportMessage } from '../../../util/testHelpers';
|
|
4
|
+
import { payloadToTransportMessage, waitForMessage, } from '../../../util/testHelpers';
|
|
6
5
|
import { ensureTransportIsClean } from '../../../__tests__/fixtures/cleanup';
|
|
7
6
|
describe('sending and receiving across node streams works', () => {
|
|
8
7
|
test('basic send/receive', async () => {
|
|
@@ -27,6 +27,7 @@ export declare class WebSocketClientTransport extends Transport<WebSocketConnect
|
|
|
27
27
|
options: Options;
|
|
28
28
|
serverId: TransportClientId;
|
|
29
29
|
reconnectPromises: Map<TransportClientId, Promise<WebSocketResult>>;
|
|
30
|
+
tryReconnecting: boolean;
|
|
30
31
|
/**
|
|
31
32
|
* Creates a new WebSocketTransport instance.
|
|
32
33
|
* @param wsGetter A function that returns a Promise that resolves to a WebSocket instance.
|
|
@@ -36,10 +37,6 @@ export declare class WebSocketClientTransport extends Transport<WebSocketConnect
|
|
|
36
37
|
constructor(wsGetter: () => Promise<WebSocket>, clientId: TransportClientId, serverId: TransportClientId, providedOptions?: Partial<Options>);
|
|
37
38
|
setupConnectionStatusListeners(): void;
|
|
38
39
|
createNewConnection(to: string, attempt?: number): Promise<void>;
|
|
39
|
-
/**
|
|
40
|
-
* Begins a new attempt to establish a WebSocket connection.
|
|
41
|
-
*/
|
|
42
|
-
open(): Promise<void>;
|
|
43
40
|
}
|
|
44
41
|
export {};
|
|
45
42
|
//# sourceMappingURL=client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../../transport/impls/ws/client.ts"],"names":[],"mappings":";AAAA,OAAO,SAAS,MAAM,eAAe,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,UAAU,OAAO;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,KAAK,CAAC;CACd;AAQD,KAAK,eAAe,GAAG;IAAE,EAAE,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3D;;;;GAIG;AACH,qBAAa,wBAAyB,SAAQ,SAAS,CAAC,mBAAmB,CAAC;IAC1E;;OAEG;IACH,QAAQ,EAAE,CAAC,EAAE,EAAE,iBAAiB,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;IACxD,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../../transport/impls/ws/client.ts"],"names":[],"mappings":";AAAA,OAAO,SAAS,MAAM,eAAe,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,UAAU,OAAO;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,KAAK,CAAC;CACd;AAQD,KAAK,eAAe,GAAG;IAAE,EAAE,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3D;;;;GAIG;AACH,qBAAa,wBAAyB,SAAQ,SAAS,CAAC,mBAAmB,CAAC;IAC1E;;OAEG;IACH,QAAQ,EAAE,CAAC,EAAE,EAAE,iBAAiB,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;IACxD,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,iBAAiB,EAAE,GAAG,CAAC,iBAAiB,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC;IACpE,eAAe,EAAE,OAAO,CAAQ;IAEhC;;;;;OAKG;gBAED,QAAQ,EAAE,MAAM,OAAO,CAAC,SAAS,CAAC,EAClC,QAAQ,EAAE,iBAAiB,EAC3B,QAAQ,EAAE,iBAAiB,EAC3B,eAAe,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC;IAWpC,8BAA8B,IAAI,IAAI;IAIhC,mBAAmB,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,SAAI;CA6ElD"}
|
|
@@ -20,6 +20,7 @@ export class WebSocketClientTransport extends Transport {
|
|
|
20
20
|
options;
|
|
21
21
|
serverId;
|
|
22
22
|
reconnectPromises;
|
|
23
|
+
tryReconnecting = true;
|
|
23
24
|
/**
|
|
24
25
|
* Creates a new WebSocketTransport instance.
|
|
25
26
|
* @param wsGetter A function that returns a Promise that resolves to a WebSocket instance.
|
|
@@ -44,6 +45,10 @@ export class WebSocketClientTransport extends Transport {
|
|
|
44
45
|
}
|
|
45
46
|
let reconnectPromise = this.reconnectPromises.get(to);
|
|
46
47
|
if (!reconnectPromise) {
|
|
48
|
+
if (!this.tryReconnecting) {
|
|
49
|
+
log?.info(`${this.clientId} -- tryReconnecting is false, not attempting reconnect`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
47
52
|
reconnectPromise = new Promise(async (resolve) => {
|
|
48
53
|
log?.info(`${this.clientId} -- establishing a new websocket to ${to}`);
|
|
49
54
|
const ws = await this.wsGetter(to);
|
|
@@ -93,10 +98,4 @@ export class WebSocketClientTransport extends Transport {
|
|
|
93
98
|
setTimeout(() => this.createNewConnection(to, attempt + 1), this.options.retryIntervalMs * attempt);
|
|
94
99
|
}
|
|
95
100
|
}
|
|
96
|
-
/**
|
|
97
|
-
* Begins a new attempt to establish a WebSocket connection.
|
|
98
|
-
*/
|
|
99
|
-
async open() {
|
|
100
|
-
return this.createNewConnection(this.serverId);
|
|
101
|
-
}
|
|
102
101
|
}
|
|
@@ -2,6 +2,7 @@ import { Codec } from '../../../codec';
|
|
|
2
2
|
import { TransportClientId } from '../../message';
|
|
3
3
|
import { Transport } from '../../transport';
|
|
4
4
|
import { Server } from 'ws';
|
|
5
|
+
import { WebSocket } from 'isomorphic-ws';
|
|
5
6
|
import { WebSocketConnection } from './connection';
|
|
6
7
|
interface Options {
|
|
7
8
|
codec: Codec;
|
|
@@ -10,8 +11,10 @@ export declare class WebSocketServerTransport extends Transport<WebSocketConnect
|
|
|
10
11
|
wss: Server;
|
|
11
12
|
clientId: TransportClientId;
|
|
12
13
|
constructor(wss: Server, clientId: TransportClientId, providedOptions?: Partial<Options>);
|
|
14
|
+
connectionHandler: (ws: WebSocket) => void;
|
|
13
15
|
setupConnectionStatusListeners(): void;
|
|
14
16
|
createNewConnection(to: string): Promise<void>;
|
|
17
|
+
close(): Promise<void>;
|
|
15
18
|
}
|
|
16
19
|
export {};
|
|
17
20
|
//# sourceMappingURL=server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../../transport/impls/ws/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAkB,MAAM,gBAAgB,CAAC;AAEvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,UAAU,OAAO;IACf,KAAK,EAAE,KAAK,CAAC;CACd;AAMD,qBAAa,wBAAyB,SAAQ,SAAS,CAAC,mBAAmB,CAAC;IAC1E,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,iBAAiB,CAAC;gBAG1B,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,iBAAiB,EAC3B,eAAe,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC;IASpC,8BAA8B,IAAI,IAAI;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../../transport/impls/ws/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAkB,MAAM,gBAAgB,CAAC;AAEvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC;AAC5B,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,UAAU,OAAO;IACf,KAAK,EAAE,KAAK,CAAC;CACd;AAMD,qBAAa,wBAAyB,SAAQ,SAAS,CAAC,mBAAmB,CAAC;IAC1E,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,iBAAiB,CAAC;gBAG1B,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,iBAAiB,EAC3B,eAAe,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC;IASpC,iBAAiB,OAAQ,SAAS,UA6BhC;IAEF,8BAA8B,IAAI,IAAI;IAIhC,mBAAmB,CAAC,EAAE,EAAE,MAAM;IAM9B,KAAK;CAIZ"}
|
|
@@ -15,34 +15,39 @@ export class WebSocketServerTransport extends Transport {
|
|
|
15
15
|
this.clientId = clientId;
|
|
16
16
|
this.setupConnectionStatusListeners();
|
|
17
17
|
}
|
|
18
|
+
connectionHandler = (ws) => {
|
|
19
|
+
let conn = undefined;
|
|
20
|
+
ws.onmessage = (msg) => {
|
|
21
|
+
// when we establish WebSocketConnection, ws.onmessage
|
|
22
|
+
// gets overriden so this only runs on the first valid message
|
|
23
|
+
// the websocket receives
|
|
24
|
+
const parsedMsg = this.parseMsg(msg.data);
|
|
25
|
+
if (parsedMsg && !conn) {
|
|
26
|
+
conn = new WebSocketConnection(this, parsedMsg.from, ws);
|
|
27
|
+
this.onConnect(conn);
|
|
28
|
+
this.handleMsg(parsedMsg);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
// close is always emitted, even on error, ok to do cleanup here
|
|
32
|
+
ws.onclose = () => {
|
|
33
|
+
if (conn) {
|
|
34
|
+
this.onDisconnect(conn);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
ws.onerror = (msg) => {
|
|
38
|
+
log?.warn(`${this.clientId} -- ws error from client ${conn?.connectedTo ?? 'unknown'}: ${msg}`);
|
|
39
|
+
};
|
|
40
|
+
};
|
|
18
41
|
setupConnectionStatusListeners() {
|
|
19
|
-
this.wss.on('connection',
|
|
20
|
-
let conn = undefined;
|
|
21
|
-
ws.onmessage = (msg) => {
|
|
22
|
-
// when we establish WebSocketConnection, ws.onmessage
|
|
23
|
-
// gets overriden so this only runs on the first valid message
|
|
24
|
-
// the websocket receives
|
|
25
|
-
const parsedMsg = this.parseMsg(msg.data);
|
|
26
|
-
if (parsedMsg && !conn) {
|
|
27
|
-
conn = new WebSocketConnection(this, parsedMsg.from, ws);
|
|
28
|
-
this.onConnect(conn);
|
|
29
|
-
this.handleMsg(parsedMsg);
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
// close is always emitted, even on error, ok to do cleanup here
|
|
33
|
-
ws.onclose = () => {
|
|
34
|
-
if (conn) {
|
|
35
|
-
this.onDisconnect(conn);
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
ws.onerror = (msg) => {
|
|
39
|
-
log?.warn(`${this.clientId} -- ws error from client ${conn?.connectedTo ?? 'unknown'}: ${msg}`);
|
|
40
|
-
};
|
|
41
|
-
});
|
|
42
|
+
this.wss.on('connection', this.connectionHandler);
|
|
42
43
|
}
|
|
43
44
|
async createNewConnection(to) {
|
|
44
45
|
const err = `${this.clientId} -- failed to send msg to ${to}, client probably dropped`;
|
|
45
46
|
log?.warn(err);
|
|
46
47
|
return;
|
|
47
48
|
}
|
|
49
|
+
async close() {
|
|
50
|
+
super.close();
|
|
51
|
+
this.wss.off('connection', this.connectionHandler);
|
|
52
|
+
}
|
|
48
53
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
|
-
import { describe, test, expect, afterAll } from 'vitest';
|
|
3
|
-
import { createWebSocketServer, createWsTransports, createDummyTransportMessage, onServerReady, createLocalWebSocketClient, } from '../../../util/testHelpers';
|
|
4
|
-
import { msg
|
|
2
|
+
import { describe, test, expect, afterAll, vi } from 'vitest';
|
|
3
|
+
import { createWebSocketServer, createWsTransports, createDummyTransportMessage, onServerReady, createLocalWebSocketClient, waitForMessage, } from '../../../util/testHelpers';
|
|
4
|
+
import { msg } from '../..';
|
|
5
5
|
import { WebSocketServerTransport } from './server';
|
|
6
6
|
import { WebSocketClientTransport } from './client';
|
|
7
|
-
import { testFinishesCleanly } from '../../../__tests__/fixtures/cleanup';
|
|
7
|
+
import { testFinishesCleanly, waitFor, } from '../../../__tests__/fixtures/cleanup';
|
|
8
8
|
describe('sending and receiving across websockets works', async () => {
|
|
9
9
|
const server = http.createServer();
|
|
10
10
|
const port = await onServerReady(server);
|
|
@@ -16,8 +16,9 @@ describe('sending and receiving across websockets works', async () => {
|
|
|
16
16
|
test('basic send/receive', async () => {
|
|
17
17
|
const [clientTransport, serverTransport] = createWsTransports(port, wss);
|
|
18
18
|
const msg = createDummyTransportMessage();
|
|
19
|
+
const msgPromise = waitForMessage(serverTransport, (recv) => recv.id === msg.id);
|
|
19
20
|
clientTransport.send(msg);
|
|
20
|
-
await expect(
|
|
21
|
+
await expect(msgPromise).resolves.toStrictEqual(msg.payload);
|
|
21
22
|
await testFinishesCleanly({
|
|
22
23
|
clientTransports: [clientTransport],
|
|
23
24
|
serverTransport,
|
|
@@ -36,8 +37,9 @@ describe('sending and receiving across websockets works', async () => {
|
|
|
36
37
|
const initClient = async (id) => {
|
|
37
38
|
const client = new WebSocketClientTransport(() => createLocalWebSocketClient(port), id, 'SERVER');
|
|
38
39
|
const initMsg = makeDummyMessage(id, serverId, 'hello server');
|
|
40
|
+
const initMsgPromise = waitForMessage(serverTransport, (recv) => recv.id === initMsg.id);
|
|
39
41
|
client.send(initMsg);
|
|
40
|
-
await expect(
|
|
42
|
+
await expect(initMsgPromise).resolves.toStrictEqual(initMsg.payload);
|
|
41
43
|
return client;
|
|
42
44
|
};
|
|
43
45
|
const client1 = await initClient(clientId1);
|
|
@@ -70,43 +72,109 @@ describe('retry logic', async () => {
|
|
|
70
72
|
// TODO: right now, we only test client-side disconnects, we probably
|
|
71
73
|
// need to also write tests for server-side crashes (but this involves clearing/restoring state)
|
|
72
74
|
// not going to worry about this rn but for future
|
|
73
|
-
test('ws
|
|
75
|
+
test('ws connection is recreated after clean disconnect', async () => {
|
|
74
76
|
const [clientTransport, serverTransport] = createWsTransports(port, wss);
|
|
75
77
|
const msg1 = createDummyTransportMessage();
|
|
76
78
|
const msg2 = createDummyTransportMessage();
|
|
79
|
+
const msg1Promise = waitForMessage(serverTransport, (recv) => recv.id === msg1.id);
|
|
77
80
|
clientTransport.send(msg1);
|
|
78
|
-
await expect(
|
|
81
|
+
await expect(msg1Promise).resolves.toStrictEqual(msg1.payload);
|
|
82
|
+
// clean disconnect
|
|
79
83
|
clientTransport.connections.forEach((conn) => conn.ws.close());
|
|
80
|
-
|
|
84
|
+
const msg2Promise = waitForMessage(serverTransport, (recv) => recv.id === msg2.id);
|
|
81
85
|
// by this point the client should have reconnected
|
|
82
|
-
|
|
86
|
+
clientTransport.send(msg2);
|
|
87
|
+
await expect(msg2Promise).resolves.toStrictEqual(msg2.payload);
|
|
83
88
|
await testFinishesCleanly({
|
|
84
89
|
clientTransports: [clientTransport],
|
|
85
90
|
serverTransport,
|
|
86
91
|
});
|
|
87
92
|
});
|
|
88
|
-
test('ws
|
|
93
|
+
test('ws connection is recreated after unclean disconnect', async () => {
|
|
89
94
|
const [clientTransport, serverTransport] = createWsTransports(port, wss);
|
|
90
95
|
const msg1 = createDummyTransportMessage();
|
|
91
96
|
const msg2 = createDummyTransportMessage();
|
|
97
|
+
const msg1Promise = waitForMessage(serverTransport, (recv) => recv.id === msg1.id);
|
|
92
98
|
clientTransport.send(msg1);
|
|
93
|
-
await expect(
|
|
99
|
+
await expect(msg1Promise).resolves.toStrictEqual(msg1.payload);
|
|
100
|
+
// unclean disconnect
|
|
94
101
|
clientTransport.connections.forEach((conn) => conn.ws.terminate());
|
|
102
|
+
const msg2Promise = waitForMessage(serverTransport, (recv) => recv.id === msg2.id);
|
|
103
|
+
// by this point the client should have reconnected
|
|
95
104
|
clientTransport.send(msg2);
|
|
105
|
+
await expect(msg2Promise).resolves.toStrictEqual(msg2.payload);
|
|
106
|
+
await testFinishesCleanly({
|
|
107
|
+
clientTransports: [clientTransport],
|
|
108
|
+
serverTransport,
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
test('both client and server transport gets connection and disconnection notifs', async () => {
|
|
112
|
+
const [clientTransport, serverTransport] = createWsTransports(port, wss);
|
|
113
|
+
const msg1 = createDummyTransportMessage();
|
|
114
|
+
const msg2 = createDummyTransportMessage();
|
|
115
|
+
const onClientConnect = vi.fn();
|
|
116
|
+
const onClientDisconnect = vi.fn();
|
|
117
|
+
const clientHandler = (evt) => {
|
|
118
|
+
if (evt.conn.connectedTo !== serverTransport.clientId)
|
|
119
|
+
return;
|
|
120
|
+
if (evt.status === 'connect')
|
|
121
|
+
return onClientConnect();
|
|
122
|
+
if (evt.status === 'disconnect')
|
|
123
|
+
return onClientDisconnect();
|
|
124
|
+
};
|
|
125
|
+
const onServerConnect = vi.fn();
|
|
126
|
+
const onServerDisconnect = vi.fn();
|
|
127
|
+
const serverHandler = (evt) => {
|
|
128
|
+
if (evt.status === 'connect' &&
|
|
129
|
+
evt.conn.connectedTo === clientTransport.clientId)
|
|
130
|
+
return onServerConnect();
|
|
131
|
+
if (evt.status === 'disconnect' &&
|
|
132
|
+
evt.conn.connectedTo === clientTransport.clientId)
|
|
133
|
+
return onServerDisconnect();
|
|
134
|
+
};
|
|
135
|
+
clientTransport.addEventListener('connectionStatus', clientHandler);
|
|
136
|
+
serverTransport.addEventListener('connectionStatus', serverHandler);
|
|
137
|
+
expect(onClientConnect).toHaveBeenCalledTimes(0);
|
|
138
|
+
expect(onClientDisconnect).toHaveBeenCalledTimes(0);
|
|
139
|
+
expect(onServerConnect).toHaveBeenCalledTimes(0);
|
|
140
|
+
expect(onServerDisconnect).toHaveBeenCalledTimes(0);
|
|
141
|
+
const msg1Promise = waitForMessage(serverTransport, (recv) => recv.id === msg1.id);
|
|
142
|
+
clientTransport.send(msg1);
|
|
143
|
+
await expect(msg1Promise).resolves.toStrictEqual(msg1.payload);
|
|
144
|
+
expect(onClientConnect).toHaveBeenCalledTimes(1);
|
|
145
|
+
expect(onClientDisconnect).toHaveBeenCalledTimes(0);
|
|
146
|
+
expect(onServerConnect).toHaveBeenCalledTimes(1);
|
|
147
|
+
expect(onServerDisconnect).toHaveBeenCalledTimes(0);
|
|
148
|
+
// clean disconnect
|
|
149
|
+
clientTransport.connections.forEach((conn) => conn.ws.close());
|
|
150
|
+
// wait for connection status to propagate to server
|
|
151
|
+
await waitFor(() => expect(onClientConnect).toHaveBeenCalledTimes(1));
|
|
152
|
+
await waitFor(() => expect(onClientDisconnect).toHaveBeenCalledTimes(1));
|
|
153
|
+
await waitFor(() => expect(onServerConnect).toHaveBeenCalledTimes(1));
|
|
154
|
+
await waitFor(() => expect(onServerDisconnect).toHaveBeenCalledTimes(1));
|
|
155
|
+
const msg2Promise = waitForMessage(serverTransport, (recv) => recv.id === msg2.id);
|
|
96
156
|
// by this point the client should have reconnected
|
|
97
|
-
|
|
98
|
-
|
|
157
|
+
clientTransport.send(msg2);
|
|
158
|
+
await expect(msg2Promise).resolves.toStrictEqual(msg2.payload);
|
|
159
|
+
expect(onClientConnect).toHaveBeenCalledTimes(2);
|
|
160
|
+
expect(onClientDisconnect).toHaveBeenCalledTimes(1);
|
|
161
|
+
expect(onServerConnect).toHaveBeenCalledTimes(2);
|
|
162
|
+
expect(onServerDisconnect).toHaveBeenCalledTimes(1);
|
|
163
|
+
// teardown
|
|
164
|
+
clientTransport.removeEventListener('connectionStatus', clientHandler);
|
|
165
|
+
serverTransport.removeEventListener('connectionStatus', serverHandler);
|
|
99
166
|
await testFinishesCleanly({
|
|
100
167
|
clientTransports: [clientTransport],
|
|
101
168
|
serverTransport,
|
|
102
169
|
});
|
|
103
170
|
});
|
|
104
|
-
test('ws
|
|
171
|
+
test('ws connection is not recreated after destroy', async () => {
|
|
105
172
|
const [clientTransport, serverTransport] = createWsTransports(port, wss);
|
|
106
173
|
const msg1 = createDummyTransportMessage();
|
|
107
174
|
const msg2 = createDummyTransportMessage();
|
|
175
|
+
const promise1 = waitForMessage(serverTransport, (recv) => recv.id === msg1.id);
|
|
108
176
|
clientTransport.send(msg1);
|
|
109
|
-
await expect(
|
|
177
|
+
await expect(promise1).resolves.toStrictEqual(msg1.payload);
|
|
110
178
|
clientTransport.destroy();
|
|
111
179
|
expect(() => clientTransport.send(msg2)).toThrow(new Error('transport is destroyed, cant send'));
|
|
112
180
|
// this is not expected to be clean because we destroyed the transport
|
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
import { OpaqueTransportMessage } from './message';
|
|
2
|
-
import { Transport, Connection } from './transport';
|
|
3
1
|
export { Transport, Connection } from './transport';
|
|
4
2
|
export { TransportMessageSchema, OpaqueTransportMessageSchema, msg, reply, } from './message';
|
|
5
3
|
export type { TransportMessage, MessageId, OpaqueTransportMessage, TransportClientId, isStreamOpen, isStreamClose, } from './message';
|
|
6
|
-
/**
|
|
7
|
-
* Waits for a message from the transport.
|
|
8
|
-
* @param {Transport} t - The transport to listen to.
|
|
9
|
-
* @param filter - An optional filter function to apply to the received messages.
|
|
10
|
-
* @returns A promise that resolves with the payload of the first message that passes the filter.
|
|
11
|
-
*/
|
|
12
|
-
export declare function waitForMessage(t: Transport<Connection>, filter?: (msg: OpaqueTransportMessage) => boolean, rejectMismatch?: boolean): Promise<unknown>;
|
|
13
4
|
//# sourceMappingURL=index.d.ts.map
|