@kitware/wslink 2.5.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/.prettierrc.js +8 -0
- package/CHANGELOG.md +551 -0
- package/LICENSE +27 -0
- package/README.md +36 -0
- package/dist/wslink.mjs +1474 -0
- package/dist/wslink.umd.js +1 -0
- package/package.json +33 -0
- package/src/CompositeClosureHelper/index.js +166 -0
- package/src/ProcessLauncher/api.md +87 -0
- package/src/ProcessLauncher/index.js +125 -0
- package/src/SmartConnect/api.md +40 -0
- package/src/SmartConnect/index.d.ts +72 -0
- package/src/SmartConnect/index.js +179 -0
- package/src/WebsocketConnection/api.md +37 -0
- package/src/WebsocketConnection/chunking.js +211 -0
- package/src/WebsocketConnection/chunking.ts +296 -0
- package/src/WebsocketConnection/index.d.ts +70 -0
- package/src/WebsocketConnection/index.js +155 -0
- package/src/WebsocketConnection/session.js +284 -0
- package/src/index.js +11 -0
- package/test/simple.js +185 -0
- package/vite.config.js +15 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// Project not setup for typescript, manually compiling this file to chunker.js
|
|
2
|
+
// npx tsc chunking.ts --target esnext
|
|
3
|
+
|
|
4
|
+
const UINT32_LENGTH = 4;
|
|
5
|
+
const ID_LOCATION = 0;
|
|
6
|
+
const ID_LENGTH = UINT32_LENGTH;
|
|
7
|
+
const MESSAGE_OFFSET_LOCATION = ID_LOCATION + ID_LENGTH;
|
|
8
|
+
const MESSAGE_OFFSET_LENGTH = UINT32_LENGTH;
|
|
9
|
+
const MESSAGE_SIZE_LOCATION = MESSAGE_OFFSET_LOCATION + MESSAGE_OFFSET_LENGTH;
|
|
10
|
+
const MESSAGE_SIZE_LENGTH = UINT32_LENGTH;
|
|
11
|
+
|
|
12
|
+
const HEADER_LENGTH = ID_LENGTH + MESSAGE_OFFSET_LENGTH + MESSAGE_SIZE_LENGTH;
|
|
13
|
+
|
|
14
|
+
function encodeHeader(id: number, offset: number, size: number): Uint8Array {
|
|
15
|
+
const buffer = new ArrayBuffer(HEADER_LENGTH);
|
|
16
|
+
const header = new Uint8Array(buffer);
|
|
17
|
+
const view = new DataView(buffer);
|
|
18
|
+
view.setUint32(ID_LOCATION, id, true);
|
|
19
|
+
view.setUint32(MESSAGE_OFFSET_LOCATION, offset, true);
|
|
20
|
+
view.setUint32(MESSAGE_SIZE_LOCATION, size, true);
|
|
21
|
+
|
|
22
|
+
return header;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function decodeHeader(header: Uint8Array) {
|
|
26
|
+
const view = new DataView(header.buffer);
|
|
27
|
+
const id = view.getUint32(ID_LOCATION, true);
|
|
28
|
+
const offset = view.getUint32(MESSAGE_OFFSET_LOCATION, true);
|
|
29
|
+
const size = view.getUint32(MESSAGE_SIZE_LOCATION, true);
|
|
30
|
+
|
|
31
|
+
return { id, offset, size };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function* generateChunks(message: Uint8Array, maxSize: number) {
|
|
35
|
+
const totalSize = message.byteLength;
|
|
36
|
+
let maxContentSize: number;
|
|
37
|
+
|
|
38
|
+
if (maxSize === 0) {
|
|
39
|
+
maxContentSize = totalSize;
|
|
40
|
+
} else {
|
|
41
|
+
maxContentSize = Math.max(maxSize - HEADER_LENGTH, 1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const id = new Uint32Array(1);
|
|
45
|
+
crypto.getRandomValues(id);
|
|
46
|
+
|
|
47
|
+
let offset = 0;
|
|
48
|
+
|
|
49
|
+
while (offset < totalSize) {
|
|
50
|
+
const contentSize = Math.min(maxContentSize, totalSize - offset);
|
|
51
|
+
const chunk = new Uint8Array(new ArrayBuffer(HEADER_LENGTH + contentSize));
|
|
52
|
+
const header = encodeHeader(id[0], offset, totalSize);
|
|
53
|
+
chunk.set(new Uint8Array(header.buffer), 0);
|
|
54
|
+
chunk.set(message.subarray(offset, offset + contentSize), HEADER_LENGTH);
|
|
55
|
+
|
|
56
|
+
yield chunk;
|
|
57
|
+
|
|
58
|
+
offset += contentSize;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type PendingMessage = {
|
|
65
|
+
receivedSize: number;
|
|
66
|
+
content: Uint8Array;
|
|
67
|
+
decoder: any;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/*
|
|
71
|
+
This un-chunker is vulnerable to DOS.
|
|
72
|
+
If it receives a message with a header claiming a large incoming message
|
|
73
|
+
it will allocate the memory blindly even without actually receiving the content
|
|
74
|
+
Chunks for a given message can come in any order
|
|
75
|
+
Chunks across messages can be interleaved.
|
|
76
|
+
*/
|
|
77
|
+
class UnChunker {
|
|
78
|
+
private pendingMessages: { [key: number]: PendingMessage };
|
|
79
|
+
|
|
80
|
+
constructor() {
|
|
81
|
+
this.pendingMessages = {};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
releasePendingMessages() {
|
|
85
|
+
this.pendingMessages = {};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async processChunk(
|
|
89
|
+
chunk: Blob,
|
|
90
|
+
decoderFactory: () => any
|
|
91
|
+
): Promise<unknown | undefined> {
|
|
92
|
+
const headerBlob = chunk.slice(0, HEADER_LENGTH);
|
|
93
|
+
const contentBlob = chunk.slice(HEADER_LENGTH);
|
|
94
|
+
|
|
95
|
+
const header = new Uint8Array(await headerBlob.arrayBuffer());
|
|
96
|
+
const { id, offset, size: totalSize } = decodeHeader(header);
|
|
97
|
+
|
|
98
|
+
let pendingMessage = this.pendingMessages[id];
|
|
99
|
+
|
|
100
|
+
if (!pendingMessage) {
|
|
101
|
+
pendingMessage = {
|
|
102
|
+
receivedSize: 0,
|
|
103
|
+
content: new Uint8Array(totalSize),
|
|
104
|
+
decoder: decoderFactory(),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
this.pendingMessages[id] = pendingMessage;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// This should never happen, but still check it
|
|
111
|
+
if (totalSize !== pendingMessage.content.byteLength) {
|
|
112
|
+
delete this.pendingMessages[id];
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Total size in chunk header for message ${id} does not match total size declared by previous chunk.`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const chunkContent = new Uint8Array(await contentBlob.arrayBuffer());
|
|
119
|
+
const content = pendingMessage.content;
|
|
120
|
+
content.set(chunkContent, offset);
|
|
121
|
+
pendingMessage.receivedSize += chunkContent.byteLength;
|
|
122
|
+
|
|
123
|
+
if (pendingMessage.receivedSize >= totalSize) {
|
|
124
|
+
delete this.pendingMessages[id];
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
return pendingMessage["decoder"].decode(content);
|
|
128
|
+
} catch (e) {
|
|
129
|
+
console.error("Malformed message: ", content.slice(0, 100));
|
|
130
|
+
// debugger;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
type StreamPendingMessage = {
|
|
139
|
+
receivedSize: number;
|
|
140
|
+
totalSize: number;
|
|
141
|
+
decoder: any;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Makes sure messages are processed in order of arrival,
|
|
145
|
+
export class SequentialTaskQueue {
|
|
146
|
+
taskId: number;
|
|
147
|
+
pendingTaskId: number;
|
|
148
|
+
tasks: {
|
|
149
|
+
[id: number]: {
|
|
150
|
+
fn: (...args: any) => Promise<any>;
|
|
151
|
+
args: any[];
|
|
152
|
+
resolve: (value: any) => void;
|
|
153
|
+
reject: (err: any) => void;
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
constructor() {
|
|
158
|
+
this.taskId = 0;
|
|
159
|
+
this.pendingTaskId = -1;
|
|
160
|
+
this.tasks = {};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
enqueue(fn: (...args: any) => Promise<any>, ...args: any[]) {
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
const taskId = this.taskId++;
|
|
166
|
+
this.tasks[taskId] = { fn, args, resolve, reject };
|
|
167
|
+
this._maybeExecuteNext();
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
_maybeExecuteNext() {
|
|
172
|
+
let pendingTask = this.tasks[this.pendingTaskId];
|
|
173
|
+
|
|
174
|
+
if (pendingTask) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const nextPendingTaskId = this.pendingTaskId + 1;
|
|
179
|
+
|
|
180
|
+
pendingTask = this.tasks[nextPendingTaskId];
|
|
181
|
+
|
|
182
|
+
if (!pendingTask) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.pendingTaskId = nextPendingTaskId;
|
|
187
|
+
|
|
188
|
+
const { fn, args, resolve, reject } = pendingTask;
|
|
189
|
+
|
|
190
|
+
fn(...args)
|
|
191
|
+
.then((result) => {
|
|
192
|
+
resolve(result);
|
|
193
|
+
delete this.tasks[nextPendingTaskId];
|
|
194
|
+
this._maybeExecuteNext();
|
|
195
|
+
})
|
|
196
|
+
.catch((err) => {
|
|
197
|
+
reject(err);
|
|
198
|
+
delete this.tasks[nextPendingTaskId];
|
|
199
|
+
this._maybeExecuteNext();
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/*
|
|
205
|
+
This un-chunker is more memory efficient
|
|
206
|
+
(each chunk is passed immediately to msgpack)
|
|
207
|
+
and it will only allocate memory when it receives content.
|
|
208
|
+
Chunks for a given message are expected to come sequentially
|
|
209
|
+
Chunks across messages can be interleaved.
|
|
210
|
+
*/
|
|
211
|
+
class StreamUnChunker {
|
|
212
|
+
private pendingMessages: { [key: number]: StreamPendingMessage };
|
|
213
|
+
|
|
214
|
+
constructor() {
|
|
215
|
+
this.pendingMessages = {};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
processChunk = async (
|
|
219
|
+
chunk: Blob,
|
|
220
|
+
decoderFactory: () => any
|
|
221
|
+
): Promise<unknown | undefined> => {
|
|
222
|
+
const headerBlob = chunk.slice(0, HEADER_LENGTH);
|
|
223
|
+
|
|
224
|
+
const header = new Uint8Array(await headerBlob.arrayBuffer());
|
|
225
|
+
const { id, offset, size: totalSize } = decodeHeader(header);
|
|
226
|
+
|
|
227
|
+
const contentBlob = chunk.slice(HEADER_LENGTH);
|
|
228
|
+
|
|
229
|
+
let pendingMessage = this.pendingMessages[id];
|
|
230
|
+
|
|
231
|
+
if (!pendingMessage) {
|
|
232
|
+
pendingMessage = {
|
|
233
|
+
receivedSize: 0,
|
|
234
|
+
totalSize: totalSize,
|
|
235
|
+
decoder: decoderFactory(),
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
this.pendingMessages[id] = pendingMessage;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// This should never happen, but still check it
|
|
242
|
+
if (totalSize !== pendingMessage.totalSize) {
|
|
243
|
+
delete this.pendingMessages[id];
|
|
244
|
+
throw new Error(
|
|
245
|
+
`Total size in chunk header for message ${id} does not match total size declared by previous chunk.`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// This should never happen, but still check it
|
|
250
|
+
if (offset !== pendingMessage.receivedSize) {
|
|
251
|
+
delete this.pendingMessages[id];
|
|
252
|
+
throw new Error(
|
|
253
|
+
`Received an unexpected chunk for message ${id}.
|
|
254
|
+
Expected offset = ${pendingMessage.receivedSize},
|
|
255
|
+
Received offset = ${offset}.`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let result: unknown;
|
|
260
|
+
try {
|
|
261
|
+
result = await pendingMessage.decoder.decodeAsync(
|
|
262
|
+
contentBlob.stream() as any
|
|
263
|
+
);
|
|
264
|
+
} catch (e) {
|
|
265
|
+
if (e instanceof RangeError) {
|
|
266
|
+
// More data is needed, it should come in the next chunk
|
|
267
|
+
result = undefined;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
pendingMessage.receivedSize += contentBlob.size;
|
|
272
|
+
|
|
273
|
+
/*
|
|
274
|
+
In principle feeding a stream to the unpacker could yield multiple outputs
|
|
275
|
+
for example unpacker.feed(b'0123') would yield b'0', b'1', etc
|
|
276
|
+
or concatenated packed payloads would yield two or more unpacked objects
|
|
277
|
+
but in our use case we expect a full message to be mapped to a single object
|
|
278
|
+
*/
|
|
279
|
+
if (result && pendingMessage.receivedSize < totalSize) {
|
|
280
|
+
delete this.pendingMessages[id];
|
|
281
|
+
throw new Error(
|
|
282
|
+
`Received a parsable payload shorter than expected for message ${id}.
|
|
283
|
+
Expected size = ${totalSize},
|
|
284
|
+
Received size = ${pendingMessage.receivedSize}.`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (pendingMessage.receivedSize >= totalSize) {
|
|
289
|
+
delete this.pendingMessages[id];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return result;
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export { UnChunker, StreamUnChunker, generateChunks };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export interface SubscriberInfo {
|
|
2
|
+
topic: string;
|
|
3
|
+
callback: (args: any[]) => Promise<any>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// Provides a generic RPC stub built on top of websocket.
|
|
7
|
+
export interface WebsocketSession {
|
|
8
|
+
// Issue a single-shot RPC.
|
|
9
|
+
call(
|
|
10
|
+
methodName: string,
|
|
11
|
+
args?: any[],
|
|
12
|
+
kwargs?: Record<string, any>
|
|
13
|
+
): Promise<any>;
|
|
14
|
+
// Subscribe to one-way messages from the server.
|
|
15
|
+
subscribe(
|
|
16
|
+
topic: string,
|
|
17
|
+
callback: (args: any[]) => Promise<any>
|
|
18
|
+
): {
|
|
19
|
+
// A promise to be resolved once subscription succeeds.
|
|
20
|
+
promise: Promise<SubscriberInfo>;
|
|
21
|
+
// Cancels the subscription.
|
|
22
|
+
unsubscribe: () => Promise<void>;
|
|
23
|
+
};
|
|
24
|
+
// Cancels the subscription.
|
|
25
|
+
unsubscribe(info: SubscriberInfo): Promise<void>;
|
|
26
|
+
close(): Promise<void>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param payload The binary data to send
|
|
30
|
+
* @returns The id assigned to the binary attachment
|
|
31
|
+
*/
|
|
32
|
+
addAttachment(payload: Blob): string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface IWebsocketConnectionInitialValues {
|
|
36
|
+
secret?: string;
|
|
37
|
+
connection?: any;
|
|
38
|
+
session?: any;
|
|
39
|
+
retry?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Represents a single established websocket connection.
|
|
43
|
+
export interface WebsocketConnection {
|
|
44
|
+
getSession(): WebsocketSession;
|
|
45
|
+
getUrl(): string | null;
|
|
46
|
+
destroy(): void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Creates a new SmartConnect object with the given configuration.
|
|
51
|
+
*/
|
|
52
|
+
export function newInstance(
|
|
53
|
+
initialValues: IWebsocketConnectionInitialValues
|
|
54
|
+
): WebsocketConnection;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Decorates a given object (publicAPI+model) with WebsocketConnection characteristics.
|
|
58
|
+
*/
|
|
59
|
+
export function extend(
|
|
60
|
+
publicAPI: object,
|
|
61
|
+
model: object,
|
|
62
|
+
initialValues?: IWebsocketConnectionInitialValues
|
|
63
|
+
): void;
|
|
64
|
+
|
|
65
|
+
export declare const WebsocketConnection: {
|
|
66
|
+
newInstance: typeof newInstance;
|
|
67
|
+
extend: typeof extend;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default WebsocketConnection;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Helper borrowed from paraviewweb/src/Common/Core
|
|
2
|
+
import CompositeClosureHelper from "../CompositeClosureHelper";
|
|
3
|
+
import Session from "./session";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_SECRET = "wslink-secret";
|
|
6
|
+
|
|
7
|
+
function getTransportObject(url) {
|
|
8
|
+
var idx = url.indexOf(":"),
|
|
9
|
+
protocol = url.substring(0, idx);
|
|
10
|
+
if (protocol === "ws" || protocol === "wss") {
|
|
11
|
+
return {
|
|
12
|
+
type: "websocket",
|
|
13
|
+
url,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Unknown protocol (${protocol}) for url (${url}). Unable to create transport object.`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function WebsocketConnection(publicAPI, model) {
|
|
23
|
+
// TODO Should we try to reconnect on error?
|
|
24
|
+
|
|
25
|
+
publicAPI.connect = () => {
|
|
26
|
+
// without a URL we can't do anything.
|
|
27
|
+
if (!model.urls) return null;
|
|
28
|
+
// concat allows a single url or a list.
|
|
29
|
+
var uriList = [].concat(model.urls),
|
|
30
|
+
transports = [];
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < uriList.length; i += 1) {
|
|
33
|
+
const url = uriList[i];
|
|
34
|
+
try {
|
|
35
|
+
const transport = getTransportObject(url);
|
|
36
|
+
transports.push(transport);
|
|
37
|
+
} catch (transportCreateError) {
|
|
38
|
+
console.error(transportCreateError);
|
|
39
|
+
publicAPI.fireConnectionError(publicAPI, transportCreateError);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (model.connection) {
|
|
45
|
+
if (model.connection.url !== transports[0].url) {
|
|
46
|
+
model.connection.close();
|
|
47
|
+
} else if (
|
|
48
|
+
model.connection.readyState === 0 ||
|
|
49
|
+
model.connection.readyState === 1
|
|
50
|
+
) {
|
|
51
|
+
// already connected.
|
|
52
|
+
return model.session;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
if (model.wsProxy) {
|
|
57
|
+
model.connection = WSLINK.createWebSocket(transports[0].url);
|
|
58
|
+
} else {
|
|
59
|
+
model.connection = new WebSocket(transports[0].url);
|
|
60
|
+
}
|
|
61
|
+
} catch (err) {
|
|
62
|
+
// If the server isn't running, we still don't enter here on Chrome -
|
|
63
|
+
// console shows a net::ERR_CONNECTION_REFUSED error inside WebSocket
|
|
64
|
+
console.error(err);
|
|
65
|
+
publicAPI.fireConnectionError(publicAPI, err);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
model.connection.binaryType = "blob";
|
|
70
|
+
if (!model.secret) model.secret = DEFAULT_SECRET;
|
|
71
|
+
model.session = Session.newInstance({
|
|
72
|
+
ws: model.connection,
|
|
73
|
+
secret: model.secret,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
model.connection.onopen = (event) => {
|
|
77
|
+
if (model.session) {
|
|
78
|
+
// sends handshake message - wait for reply before issuing ready()
|
|
79
|
+
model.session.onconnect(event).then(
|
|
80
|
+
() => {
|
|
81
|
+
publicAPI.fireConnectionReady(publicAPI);
|
|
82
|
+
},
|
|
83
|
+
(err) => {
|
|
84
|
+
console.error("Connection error", err);
|
|
85
|
+
publicAPI.fireConnectionError(publicAPI, err);
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
model.connection.onclose = (event) => {
|
|
92
|
+
publicAPI.fireConnectionClose(publicAPI, event);
|
|
93
|
+
model.connection = null;
|
|
94
|
+
// return !model.retry; // true => Stop retry
|
|
95
|
+
};
|
|
96
|
+
model.connection.onerror = (event) => {
|
|
97
|
+
publicAPI.fireConnectionError(publicAPI, event);
|
|
98
|
+
};
|
|
99
|
+
// handle messages in the session.
|
|
100
|
+
model.connection.onmessage = (event) => {
|
|
101
|
+
model.session.onmessage(event);
|
|
102
|
+
};
|
|
103
|
+
return model.session;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
publicAPI.getSession = () => model.session;
|
|
107
|
+
|
|
108
|
+
publicAPI.getUrl = () =>
|
|
109
|
+
model.connection ? model.connection.url : undefined;
|
|
110
|
+
|
|
111
|
+
function cleanUp(timeout = 10) {
|
|
112
|
+
if (
|
|
113
|
+
model.connection &&
|
|
114
|
+
model.connection.readyState === 1 &&
|
|
115
|
+
model.session &&
|
|
116
|
+
timeout > 0
|
|
117
|
+
) {
|
|
118
|
+
model.session.call("application.exit.later", [timeout]);
|
|
119
|
+
}
|
|
120
|
+
if (model.connection) {
|
|
121
|
+
model.connection.close();
|
|
122
|
+
}
|
|
123
|
+
model.connection = null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
publicAPI.destroy = CompositeClosureHelper.chain(cleanUp, publicAPI.destroy);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const DEFAULT_VALUES = {
|
|
130
|
+
secret: DEFAULT_SECRET,
|
|
131
|
+
connection: null,
|
|
132
|
+
session: null,
|
|
133
|
+
retry: false,
|
|
134
|
+
wsProxy: false, // Use WSLINK.WebSocket if true else native WebSocket
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export function extend(publicAPI, model, initialValues = {}) {
|
|
138
|
+
Object.assign(model, DEFAULT_VALUES, initialValues);
|
|
139
|
+
|
|
140
|
+
CompositeClosureHelper.destroy(publicAPI, model);
|
|
141
|
+
CompositeClosureHelper.event(publicAPI, model, "ConnectionReady");
|
|
142
|
+
CompositeClosureHelper.event(publicAPI, model, "ConnectionClose");
|
|
143
|
+
CompositeClosureHelper.event(publicAPI, model, "ConnectionError");
|
|
144
|
+
CompositeClosureHelper.isA(publicAPI, model, "WebsocketConnection");
|
|
145
|
+
|
|
146
|
+
WebsocketConnection(publicAPI, model);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ----------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
export const newInstance = CompositeClosureHelper.newInstance(extend);
|
|
152
|
+
|
|
153
|
+
// ----------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
export default { newInstance, extend };
|