@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.
@@ -0,0 +1,284 @@
1
+ // Helper borrowed from paraviewweb/src/Common/Core
2
+ import CompositeClosureHelper from "../CompositeClosureHelper";
3
+ import { UnChunker, generateChunks } from "./chunking";
4
+ import { Encoder, Decoder } from "@msgpack/msgpack";
5
+
6
+ function defer() {
7
+ const deferred = {};
8
+
9
+ deferred.promise = new Promise(function (resolve, reject) {
10
+ deferred.resolve = resolve;
11
+ deferred.reject = reject;
12
+ });
13
+
14
+ return deferred;
15
+ }
16
+
17
+ function Session(publicAPI, model) {
18
+ const CLIENT_ERROR = -32099;
19
+ let msgCount = 0;
20
+ const inFlightRpc = {};
21
+ // matches 'rpc:client3:21'
22
+ // client may be dot-separated and include '_'
23
+ // number is message count - unique.
24
+ // matches 'publish:dot.separated.topic:42'
25
+ const regexRPC = /^(rpc|publish|system):(\w+(?:\.\w+)*):(?:\d+)$/;
26
+ const subscriptions = {};
27
+ let clientID = null;
28
+ let MAX_MSG_SIZE = 512 * 1024;
29
+ const unchunker = new UnChunker();
30
+
31
+ // --------------------------------------------------------------------------
32
+ // Private helpers
33
+ // --------------------------------------------------------------------------
34
+
35
+ function onCompleteMessage(payload) {
36
+ if (!payload) return;
37
+ if (!payload.id) return;
38
+ if (payload.error) {
39
+ const deferred = inFlightRpc[payload.id];
40
+ if (deferred) {
41
+ deferred.reject(payload.error);
42
+ } else {
43
+ console.error("Server error:", payload.error);
44
+ }
45
+ } else {
46
+ const match = regexRPC.exec(payload.id);
47
+ if (match) {
48
+ const type = match[1];
49
+ if (type === "rpc") {
50
+ const deferred = inFlightRpc[payload.id];
51
+ if (!deferred) {
52
+ console.log(
53
+ "session message id without matching call, dropped",
54
+ payload
55
+ );
56
+ return;
57
+ }
58
+ deferred.resolve(payload.result);
59
+ } else if (type == "publish") {
60
+ console.assert(
61
+ inFlightRpc[payload.id] === undefined,
62
+ "publish message received matching in-flight rpc call"
63
+ );
64
+ // regex extracts the topic for us.
65
+ const topic = match[2];
66
+ if (!subscriptions[topic]) {
67
+ return;
68
+ }
69
+ // for each callback, provide the message data. Wrap in an array, for back-compatibility with WAMP
70
+ subscriptions[topic].forEach((callback) =>
71
+ callback([payload.result])
72
+ );
73
+ } else if (type == "system") {
74
+ // console.log('DBG system:', payload.id, payload.result);
75
+ const deferred = inFlightRpc[payload.id];
76
+ if (payload.id === "system:c0:0") {
77
+ clientID = payload.result.clientID;
78
+ MAX_MSG_SIZE = payload.result.maxMsgSize || MAX_MSG_SIZE;
79
+ if (deferred) deferred.resolve(clientID);
80
+ } else {
81
+ console.error("Unknown system message", payload.id);
82
+ if (deferred)
83
+ deferred.reject({
84
+ code: CLIENT_ERROR,
85
+ message: `Unknown system message ${payload.id}`,
86
+ });
87
+ }
88
+ } else {
89
+ console.error("Unknown rpc id format", payload.id);
90
+ }
91
+ }
92
+ }
93
+ delete inFlightRpc[payload.id];
94
+ }
95
+
96
+ // --------------------------------------------------------------------------
97
+ // Public API
98
+ // --------------------------------------------------------------------------
99
+
100
+ publicAPI.onconnect = (event) => {
101
+ // send hello message
102
+ const deferred = defer();
103
+ const id = "system:c0:0";
104
+ inFlightRpc[id] = deferred;
105
+
106
+ const wrapper = {
107
+ wslink: "1.0",
108
+ id,
109
+ method: "wslink.hello",
110
+ args: [{ secret: model.secret }],
111
+ kwargs: {},
112
+ };
113
+
114
+ const encoder = new CustomEncoder();
115
+ const packedWrapper = encoder.encode(wrapper);
116
+
117
+ for (let chunk of generateChunks(packedWrapper, MAX_MSG_SIZE)) {
118
+ model.ws.send(chunk, { binary: true });
119
+ }
120
+
121
+ return deferred.promise;
122
+ };
123
+
124
+ // --------------------------------------------------------------------------
125
+
126
+ publicAPI.call = (method, args = [], kwargs = {}) => {
127
+ // create a promise that we will use to notify the caller of the result.
128
+ const deferred = defer();
129
+ // readyState OPEN === 1
130
+ if (model.ws && clientID && model.ws.readyState === 1) {
131
+ const id = `rpc:${clientID}:${msgCount++}`;
132
+ inFlightRpc[id] = deferred;
133
+
134
+ const wrapper = { wslink: "1.0", id, method, args, kwargs };
135
+
136
+ const encoder = new CustomEncoder();
137
+ const packedWrapper = encoder.encode(wrapper);
138
+
139
+ for (let chunk of generateChunks(packedWrapper, MAX_MSG_SIZE)) {
140
+ model.ws.send(chunk, { binary: true });
141
+ }
142
+ } else {
143
+ deferred.reject({
144
+ code: CLIENT_ERROR,
145
+ message: `RPC call ${method} unsuccessful: connection not open`,
146
+ });
147
+ }
148
+ return deferred.promise;
149
+ };
150
+
151
+ // --------------------------------------------------------------------------
152
+
153
+ publicAPI.subscribe = (topic, callback) => {
154
+ const deferred = defer();
155
+ if (model.ws && clientID) {
156
+ // we needs to track subscriptions, to trigger callback when publish is received.
157
+ if (!subscriptions[topic]) subscriptions[topic] = [];
158
+ subscriptions[topic].push(callback);
159
+ // we can notify the server, but we don't need to, if the server always sends messages unconditionally.
160
+ // model.ws.send(JSON.stringify({ wslink: '1.0', id: `subscribe:${msgCount++}`, method, args: [] }));
161
+ deferred.resolve({ topic, callback });
162
+ } else {
163
+ deferred.reject({
164
+ code: CLIENT_ERROR,
165
+ message: `Subscribe call ${topic} unsuccessful: connection not open`,
166
+ });
167
+ }
168
+ return {
169
+ topic,
170
+ callback,
171
+ promise: deferred.promise,
172
+ unsubscribe: () => publicAPI.unsubscribe({ topic, callback }),
173
+ };
174
+ };
175
+
176
+ // --------------------------------------------------------------------------
177
+
178
+ publicAPI.unsubscribe = (info) => {
179
+ const deferred = defer();
180
+ const { topic, callback } = info;
181
+ if (!subscriptions[topic]) {
182
+ deferred.reject({
183
+ code: CLIENT_ERROR,
184
+ message: `Unsubscribe call ${topic} unsuccessful: not subscribed`,
185
+ });
186
+ return deferred.promise;
187
+ }
188
+ const index = subscriptions[topic].indexOf(callback);
189
+ if (index !== -1) {
190
+ subscriptions[topic].splice(index, 1);
191
+ deferred.resolve();
192
+ } else {
193
+ deferred.reject({
194
+ code: CLIENT_ERROR,
195
+ message: `Unsubscribe call ${topic} unsuccessful: callback not found`,
196
+ });
197
+ }
198
+ return deferred.promise;
199
+ };
200
+
201
+ // --------------------------------------------------------------------------
202
+
203
+ publicAPI.close = () => {
204
+ const deferred = defer();
205
+ // some transports might be able to close the session without closing the connection. Not true for websocket...
206
+ model.ws.close();
207
+ unchunker.releasePendingMessages();
208
+ deferred.resolve();
209
+ return deferred.promise;
210
+ };
211
+
212
+ // --------------------------------------------------------------------------
213
+
214
+ function createDecoder() {
215
+ return new Decoder();
216
+ }
217
+
218
+ publicAPI.onmessage = async (event) => {
219
+ const message = await unchunker.processChunk(event.data, createDecoder);
220
+
221
+ if (message) {
222
+ onCompleteMessage(message);
223
+ }
224
+ };
225
+
226
+ // --------------------------------------------------------------------------
227
+
228
+ publicAPI.addAttachment = (payload) => {
229
+ // Deprecated method, keeping it to avoid breaking compatibility
230
+ // Now that we use msgpack to pack/unpack messages,
231
+ // We can have binary data directly in the object itself,
232
+ // without needing to transfer it separately from the rest.
233
+ //
234
+ // If an ArrayBuffer is passed, ensure it gets wrapped in
235
+ // a DataView (which is what the encoder expects).
236
+ if (payload instanceof ArrayBuffer) {
237
+ return new DataView(payload);
238
+ }
239
+
240
+ return payload;
241
+ };
242
+ }
243
+
244
+ const DEFAULT_VALUES = {
245
+ secret: "wslink-secret",
246
+ ws: null,
247
+ };
248
+
249
+ export function extend(publicAPI, model, initialValues = {}) {
250
+ Object.assign(model, DEFAULT_VALUES, initialValues);
251
+
252
+ CompositeClosureHelper.destroy(publicAPI, model);
253
+ CompositeClosureHelper.isA(publicAPI, model, "Session");
254
+
255
+ Session(publicAPI, model);
256
+ }
257
+
258
+ // ----------------------------------------------------------------------------
259
+
260
+ export const newInstance = CompositeClosureHelper.newInstance(extend);
261
+
262
+ // ----------------------------------------------------------------------------
263
+
264
+ export default { newInstance, extend };
265
+
266
+ class CustomEncoder extends Encoder {
267
+ // Unfortunately @msgpack/msgpack only supports
268
+ // views of an ArrayBuffer (DataView, Uint8Array,..),
269
+ // but not an ArrayBuffer itself.
270
+ // They suggest using custom type extensions to support it,
271
+ // but that would yield a different packed payload
272
+ // (1 byte larger, but most importantly it would require
273
+ // dealing with the custom type when unpacking on the server).
274
+ // Since this type is too trivial to be treated differently,
275
+ // and since I don't want to rely on the users always wrapping
276
+ // their ArrayBuffers in a view, I'm subclassing the encoder.
277
+ encodeObject(object, depth) {
278
+ if (object instanceof ArrayBuffer) {
279
+ object = new DataView(object);
280
+ }
281
+
282
+ return super.encodeObject.call(this, object, depth);
283
+ }
284
+ }
package/src/index.js ADDED
@@ -0,0 +1,11 @@
1
+ import CompositeClosureHelper from "./CompositeClosureHelper";
2
+ import ProcessLauncher from "./ProcessLauncher";
3
+ import SmartConnect from "./SmartConnect";
4
+ import WebsocketConnection from "./WebsocketConnection";
5
+
6
+ export {
7
+ CompositeClosureHelper,
8
+ ProcessLauncher,
9
+ SmartConnect,
10
+ WebsocketConnection,
11
+ };
package/test/simple.js ADDED
@@ -0,0 +1,185 @@
1
+ /* global document */
2
+ import WebsocketConnection from "../src/WebsocketConnection";
3
+ import SmartConnect from "../src/SmartConnect";
4
+
5
+ // this template allows us to use HtmlWebpackPlugin
6
+ // in webpack to generate our index.html
7
+ // expose-loader makes our 'export' functions part of the 'app' global
8
+ const htmlContent = `<button onClick="app.connect()">Connect</button>
9
+ <button onClick="app.wsclose()">Disconnect</button>
10
+ <br/>
11
+ <input type="text" value="1,2,3,4,5" class="input" />
12
+ <button onClick="app.sendInput('add')">Send Add</button>
13
+ <button onClick="app.sendInput('mult')">Send Mult</button>
14
+ <button onClick="app.sendImage('unwrapped.image')">Send Image</button>
15
+ <button onClick="app.testNesting()">Test Nesting</button>
16
+ <button onClick="app.toggleStream()">Sub/Unsub</button>
17
+ <button onClick="app.sendMistake()">Mistake</button>
18
+ <button onClick="app.sendInput('special')">Test NaN</button>
19
+ <button onClick="app.sendServerQuit()">Server Quit</button>
20
+ <br/>
21
+ <textarea class="output" rows="12" cols="50"></textarea>
22
+ <br/>
23
+ <canvas class="imageCanvas" width="300px" height="300px"></canvas>
24
+ `;
25
+
26
+ const rootContainer = document.querySelector("body");
27
+ const controlContainer = document.createElement("div");
28
+ rootContainer.appendChild(controlContainer);
29
+ controlContainer.innerHTML = htmlContent;
30
+
31
+ const inputElement = document.querySelector(".input");
32
+ const logOutput = document.querySelector(".output");
33
+ let ws = null;
34
+ let subscription = false;
35
+ let session = null;
36
+
37
+ function log(msg) {
38
+ console.log(msg);
39
+ logOutput.innerHTML += msg;
40
+ logOutput.innerHTML += "\n";
41
+ }
42
+ function logerr(err) {
43
+ console.error(err);
44
+ logOutput.innerHTML += `error: ${err.code}, "${err.message}", ${err.data}`;
45
+ logOutput.innerHTML += "\n";
46
+ }
47
+
48
+ export function sendInput(type) {
49
+ if (!session) return;
50
+ const data = JSON.parse("[" + inputElement.value + "]");
51
+ session.call(`myprotocol.${type}`, [data]).then(
52
+ (result) => log("result " + result),
53
+ (err) => logerr(err)
54
+ );
55
+ }
56
+ export function sendImage(type) {
57
+ if (!session) return;
58
+ session.call(`myprotocol.${type}`, []).then(
59
+ (result) => {
60
+ log("result " + result);
61
+ handleMessage(result);
62
+ },
63
+ (err) => logerr(err)
64
+ );
65
+ }
66
+ function handleMessage(inData) {
67
+ let data = Array.isArray(inData) ? inData[0] : inData;
68
+ let blob = data.blob || data;
69
+ if (blob instanceof Blob) {
70
+ const canvas = document.querySelector(".imageCanvas");
71
+ const ctx = canvas.getContext("2d");
72
+
73
+ const img = new Image();
74
+ const reader = new FileReader();
75
+ reader.onload = function (e) {
76
+ img.onload = () => ctx.drawImage(img, 0, 0);
77
+ img.src = e.target.result;
78
+ };
79
+ reader.readAsDataURL(blob);
80
+ } else {
81
+ log("result " + blob);
82
+ }
83
+ }
84
+
85
+ export function testNesting() {
86
+ if (!session) return;
87
+ session.call("myprotocol.nested.image", []).then(
88
+ (data) => {
89
+ if (data["image"]) handleMessage(data["image"]);
90
+ const onload = (e) => {
91
+ const arr = new Uint8Array(e.target.result);
92
+ if (arr.length === 4) {
93
+ arr.forEach((d, i) => {
94
+ if (d !== i + 1) console.error("mismatch4", d, i);
95
+ });
96
+ } else if (arr.length === 6) {
97
+ arr.forEach((d, i) => {
98
+ if (d !== i + 5) console.error("mismatch4", d, i);
99
+ });
100
+ } else {
101
+ console.error("Size mismatch", arr.length);
102
+ }
103
+ };
104
+ data.bytesList.forEach((bl) => {
105
+ const reader = new FileReader();
106
+ reader.onload = onload;
107
+ reader.readAsArrayBuffer(bl);
108
+ });
109
+
110
+ console.log("Nesting:", data);
111
+ },
112
+ (err) => logerr(err)
113
+ );
114
+ }
115
+
116
+ export function sendMistake() {
117
+ if (!session) return;
118
+ session
119
+ .call("myprotocol.mistake.TYPO", ["ignored"])
120
+ .then(handleMessage, (err) => logerr(err));
121
+ }
122
+
123
+ export function sendServerQuit() {
124
+ if (!session) return;
125
+ session.call("application.exit.later", [5]).then(
126
+ (result) => log("result " + result),
127
+ (err) => logerr(err)
128
+ );
129
+ }
130
+
131
+ export function toggleStream() {
132
+ if (!subscription) {
133
+ subscription = session.subscribe("image", handleMessage);
134
+ session.call("myprotocol.stream", ["image"]).then(
135
+ (result) => log("result " + result),
136
+ (err) => logerr(err)
137
+ );
138
+ } else {
139
+ session.call("myprotocol.stop", ["image"]).then(
140
+ (result) => log("result " + result),
141
+ (err) => logerr(err)
142
+ );
143
+ // session.unsubscribe(subscription);
144
+ subscription.unsubscribe();
145
+ subscription = null;
146
+ }
147
+ }
148
+
149
+ export function wsclose() {
150
+ if (!session) return;
151
+ session.close();
152
+ // it's fine to destroy the WebsocketConnection, but you won't get the WS close message.
153
+ // if (ws) ws.destroy();
154
+ // ws = null;
155
+ }
156
+
157
+ export function connect(direct = false) {
158
+ ws = null;
159
+ if (direct) {
160
+ ws = WebsocketConnection.newInstance({ urls: "ws://localhost:8080/ws" });
161
+ } else {
162
+ const config = { application: "simple" };
163
+ ws = SmartConnect.newInstance({ config });
164
+ }
165
+ ws.onConnectionReady(() => {
166
+ log("WS open");
167
+ if (!session) {
168
+ session = ws.getSession();
169
+ }
170
+ const canvas = document.querySelector(".imageCanvas");
171
+ const ctx = canvas.getContext("2d");
172
+ ctx.clearRect(0, 0, 300, 300);
173
+ });
174
+
175
+ ws.onConnectionClose(() => {
176
+ log("WS close");
177
+ });
178
+
179
+ ws.onConnectionError((event) => {
180
+ log("WS error");
181
+ console.error(event);
182
+ });
183
+
184
+ session = ws.connect();
185
+ }
package/vite.config.js ADDED
@@ -0,0 +1,15 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { defineConfig } from "vite";
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+
7
+ export default defineConfig({
8
+ build: {
9
+ lib: {
10
+ entry: resolve(__dirname, "src/index.js"),
11
+ name: "wslink",
12
+ fileName: "wslink",
13
+ },
14
+ },
15
+ });