scoundrel-remote-eval 1.0.6 → 1.0.8

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,18 @@
1
+ import js from "@eslint/js"
2
+ import globals from "globals"
3
+ import {defineConfig} from "eslint/config"
4
+
5
+ export default defineConfig([
6
+ {
7
+ files: ["**/*.{js,mjs,cjs}"],
8
+ plugins: {js},
9
+ extends: ["js/recommended"],
10
+ languageOptions: {
11
+ globals: {
12
+ ...globals.browser,
13
+ ...globals.node,
14
+ ...globals.jasmine
15
+ }
16
+ }
17
+ }
18
+ ])
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "scoundrel-remote-eval",
3
3
  "type": "module",
4
- "version": "1.0.6",
4
+ "version": "1.0.8",
5
5
  "description": "",
6
6
  "main": "src/index.js",
7
7
  "scripts": {
8
+ "lint": "eslint",
9
+ "typecheck": "tsc --noEmit",
8
10
  "test": "jasmine"
9
11
  },
10
12
  "repository": {
@@ -21,7 +23,13 @@
21
23
  },
22
24
  "homepage": "https://github.com/kaspernj/scoundrel#readme",
23
25
  "devDependencies": {
24
- "jasmine": "^5.1.0"
26
+ "@eslint/js": "^9.39.2",
27
+ "@types/jasmine": "^5.1.13",
28
+ "@types/node": "^25.0.2",
29
+ "eslint": "^9.39.2",
30
+ "globals": "^16.5.0",
31
+ "jasmine": "^5.1.0",
32
+ "typescript": "^5.9.3"
25
33
  },
26
34
  "dependencies": {
27
35
  "ws": "^8.14.2"
@@ -1,3 +1,5 @@
1
+ // @ts-check
2
+
1
3
  import Client from "../src/client/index.js"
2
4
  import ClientWebSocket from "../src/client/connections/web-socket/index.js"
3
5
  import referenceWithProxy from "../src/client/reference-proxy.js"
@@ -36,9 +38,13 @@ describe("referenceWithProxy", () => {
36
38
  const stringObjectReference = await shared.client.newObjectWithReference("Array")
37
39
  const stringObject = referenceWithProxy(stringObjectReference)
38
40
 
41
+ // @ts-ignore
39
42
  await stringObject.push("test1")
43
+
44
+ // @ts-ignore
40
45
  await stringObject.push("test2")
41
46
 
47
+ // @ts-ignore
42
48
  const result = await stringObject.__serialize()
43
49
 
44
50
  expect(result).toEqual(["test1", "test2"])
@@ -65,9 +71,13 @@ describe("referenceWithProxy", () => {
65
71
  const stringObjectReference = await shared.client.newObjectWithReference("Array")
66
72
  const stringObject = referenceWithProxy(stringObjectReference)
67
73
 
74
+ // @ts-ignore
68
75
  await stringObject.push("test1")
76
+
77
+ // @ts-ignore
69
78
  await stringObject.push("test2")
70
79
 
80
+ // @ts-ignore
71
81
  const result = await stringObject.__serialize()
72
82
 
73
83
  expect(result).toEqual(["test1", "test2"])
@@ -77,9 +87,13 @@ describe("referenceWithProxy", () => {
77
87
  const stringObjectReference = await shared.serverClient.newObjectWithReference("Array")
78
88
  const stringObject = referenceWithProxy(stringObjectReference)
79
89
 
90
+ // @ts-ignore
80
91
  await stringObject.push("test1")
92
+
93
+ // @ts-ignore
81
94
  await stringObject.push("test2")
82
95
 
96
+ // @ts-ignore
83
97
  const result = await stringObject.__serialize()
84
98
 
85
99
  expect(result).toEqual(["test1", "test2"])
@@ -1,3 +1,5 @@
1
+ // @ts-check
2
+
1
3
  import Client from "../src/client/index.js"
2
4
  import ClientWebSocket from "../src/client/connections/web-socket/index.js"
3
5
  import Server from "../src/server/index.js"
@@ -46,4 +48,19 @@ describe("scoundrel - web-socket - javascript", () => {
46
48
 
47
49
  expect(result).toEqual("test1, test2")
48
50
  })
51
+
52
+ it("handles errors from method calls", async () => {
53
+ const stringObject = await shared.client.newObjectWithReference("Array")
54
+
55
+ let caughtError = null
56
+
57
+ try {
58
+ await stringObject.callMethod("nonExistentMethod")
59
+ } catch (error) {
60
+ caughtError = error
61
+ }
62
+
63
+ expect(caughtError).toBeInstanceOf(Error)
64
+ expect(caughtError.message).toEqual("No method called 'nonExistentMethod' on a 'Array'")
65
+ })
49
66
  })
@@ -1,3 +1,5 @@
1
+ // @ts-check
2
+
1
3
  import Client from "../src/client/index.js"
2
4
  import ClientWebSocket from "../src/client/connections/web-socket/index.js"
3
5
  import Logger from "../src/logger.js"
@@ -1,3 +1,5 @@
1
+ // @ts-check
2
+
1
3
  import Logger from "../../../logger.js"
2
4
 
3
5
  const logger = new Logger("Scoundrel WebSocket")
@@ -5,10 +7,20 @@ const logger = new Logger("Scoundrel WebSocket")
5
7
  // logger.setDebug(true)
6
8
 
7
9
  export default class WebSocket {
10
+ /**
11
+ * Creates a new WebSocket connection handler
12
+ * @param {WebSocket} ws The WebSocket instance
13
+ */
8
14
  constructor(ws) {
9
15
  this.ws = ws
16
+
17
+ // @ts-ignore
10
18
  this.ws.addEventListener("error", this.onSocketError)
19
+
20
+ // @ts-ignore
11
21
  this.ws.addEventListener("open", this.onSocketOpen)
22
+
23
+ // @ts-ignore
12
24
  this.ws.addEventListener("message", this.onSocketMessage)
13
25
 
14
26
  this.commands = {}
@@ -19,33 +31,58 @@ export default class WebSocket {
19
31
  await this.ws.close()
20
32
  }
21
33
 
34
+ /**
35
+ * @param {(data: any) => void} callback
36
+ */
22
37
  onCommand(callback) {
23
38
  this.onCommandCallback = callback
24
39
  }
25
40
 
41
+ /**
42
+ * @param {Event} event
43
+ */
26
44
  onSocketError = (event) => {
27
45
  logger.error(() => ["onSocketError", event])
28
46
  }
29
47
 
48
+ /**
49
+ * @param {MessageEvent} event
50
+ */
30
51
  onSocketMessage = (event) => {
31
52
  const data = JSON.parse(event.data)
32
53
 
33
54
  logger.log(() => ["Client::Connections::WebSocket onSocketMessage", data])
55
+
56
+ if (!this.onCommandCallback) {
57
+ throw new Error("No onCommand callback set, ignoring message")
58
+ }
59
+
34
60
  this.onCommandCallback(data)
35
61
  }
36
62
 
37
- onSocketOpen = (_event) => {
63
+ /**
64
+ * @param {Event} _event
65
+ */
66
+ onSocketOpen = (_event) => { // eslint-disable-line no-unused-vars
38
67
  logger.log("onSocketOpen")
39
68
  }
40
69
 
70
+ /**
71
+ * @param {Record<string, any>} data
72
+ */
41
73
  send(data) {
42
74
  const sendData = JSON.stringify(data)
43
75
  logger.log(() => ["Sending", sendData])
76
+
77
+ // @ts-ignore
44
78
  this.ws.send(sendData)
45
79
  }
46
80
 
47
81
  waitForOpened = () => new Promise((resolve, reject) => {
82
+ // @ts-ignore
48
83
  this.ws.addEventListener("open", resolve)
84
+
85
+ // @ts-ignore
49
86
  this.ws.addEventListener("error", reject)
50
87
  })
51
88
  }
@@ -1,3 +1,5 @@
1
+ // @ts-check
2
+
1
3
  import Logger from "../logger.js"
2
4
  import Reference from "./reference.js"
3
5
 
@@ -15,14 +17,23 @@ export default class Client {
15
17
  this.backend = backend
16
18
  this.backend.onCommand(this.onCommand)
17
19
 
20
+ /** @type {Record<number, any>} */
18
21
  this.outgoingCommands = {}
19
22
  this.incomingCommands = {}
20
23
  this.outgoingCommandsCount = 0
21
24
 
25
+ /** @type {Record<string, any>} */
22
26
  this._classes = {}
27
+
28
+ /** @type {Record<string, any>} */
23
29
  this._objects = {}
30
+
31
+ /** @type {Record<string, Reference>} */
24
32
  this.references = {}
33
+
34
+ /** @type {Record<number, any>} */
25
35
  this.objects = {}
36
+
26
37
  this.objectsCount = 0
27
38
  }
28
39
 
@@ -130,7 +141,8 @@ export default class Client {
130
141
  /**
131
142
  * Spawns a new reference to an object
132
143
  *
133
- * @param {string} id
144
+ * @param {string} className
145
+ * @param {...any} args
134
146
  * @returns {Promise<Reference>}
135
147
  */
136
148
  async newObjectWithReference(className, ...args) {
@@ -148,6 +160,11 @@ export default class Client {
148
160
  return this.spawnReference(id)
149
161
  }
150
162
 
163
+ /**
164
+ * Checks if the input is a plain object
165
+ * @param {any} input
166
+ * @returns {boolean}
167
+ */
151
168
  isPlainObject(input) {
152
169
  if (input && typeof input === "object" && !Array.isArray(input)) {
153
170
  return true
@@ -156,7 +173,18 @@ export default class Client {
156
173
  return false
157
174
  }
158
175
 
159
- onCommand = ({command, command_id: commandID, data, ...restArgs}) => {
176
+ /**
177
+ * Handles an incoming command from the backend
178
+ * @param {object} args
179
+ * @param {string} args.command
180
+ * @param {number} args.command_id
181
+ * @param {any} args.data
182
+ * @param {string} [args.error]
183
+ * @param {string} [args.errorStack]
184
+ */
185
+ onCommand = ({command, command_id: commandID, data, error, errorStack, ...restArgs}) => {
186
+ logger.log(() => ["onCommand", {command, commandID, data, error, errorStack, restArgs}])
187
+
160
188
  try {
161
189
  if (!command) {
162
190
  throw new Error(`No command key given in data: ${Object.keys(restArgs).join(", ")}`)
@@ -167,7 +195,7 @@ export default class Client {
167
195
  if (serverObject) {
168
196
  object = serverObject
169
197
  } else {
170
- object = global[data.object_name]
198
+ object = globalThis[data.object_name]
171
199
 
172
200
  if (!object) throw new Error(`No such object: ${data.object_name}`)
173
201
  }
@@ -181,7 +209,7 @@ export default class Client {
181
209
  let object
182
210
 
183
211
  if (typeof className == "string") {
184
- const ClassInstance = this.getClass(className) || global[className]
212
+ const ClassInstance = this.getClass(className) || globalThis[className]
185
213
 
186
214
  if (!ClassInstance) throw new Error(`No such class: ${className}`)
187
215
 
@@ -233,20 +261,22 @@ export default class Client {
233
261
  this.respondToCommand(commandID, {response: attribute})
234
262
  }
235
263
  } else if (command == "command_response") {
236
- if (!(commandID in this.outgoingCommands)) throw new Error(`Outgoing command ${commandID} not found`)
264
+ if (!(commandID in this.outgoingCommands)) {
265
+ throw new Error(`Outgoing command ${commandID} not found: ${Object.keys(this.outgoingCommands).join(", ")}`)
266
+ }
237
267
 
238
268
  const savedCommand = this.outgoingCommands[commandID]
239
269
 
240
270
  delete this.outgoingCommands[commandID]
241
271
 
242
- if (data.error) {
243
- const error = new Error(data.error)
272
+ if (error) {
273
+ const errorToThrow = new Error(error)
244
274
 
245
- if (data.errorStack) {
246
- error.stack = `${data.errorStack}\n\n${error.stack}`
275
+ if (errorStack) {
276
+ errorToThrow.stack = `${errorStack}\n\n${errorToThrow.stack}`
247
277
  }
248
278
 
249
- savedCommand.reject(error)
279
+ savedCommand.reject(errorToThrow)
250
280
  } else {
251
281
  logger.log(() => [`Resolving command ${commandID} with data`, data])
252
282
  savedCommand.resolve(data.data)
@@ -255,9 +285,13 @@ export default class Client {
255
285
  throw new Error(`Unknown command: ${command}`)
256
286
  }
257
287
  } catch (error) {
258
- this.send({command: "command_response", command_id: commandID, error: `Unknown command: ${error.message}`, errorStack: error.stack})
288
+ if (error instanceof Error) {
289
+ this.send({command: "command_response", command_id: commandID, error: error.message, errorStack: error.stack})
290
+ } else {
291
+ this.send({command: "command_response", command_id: commandID, error: String(error)})
292
+ }
259
293
 
260
- console.error(error)
294
+ logger.error(error)
261
295
  }
262
296
  }
263
297
 
@@ -276,6 +310,7 @@ export default class Client {
276
310
  __scoundrel_type: "reference"
277
311
  }
278
312
  } else if (this.isPlainObject(arg)) {
313
+ /** @type {Record<any, any>} */
279
314
  const newObject = {}
280
315
 
281
316
  for (const key in arg) {
@@ -368,10 +403,21 @@ export default class Client {
368
403
  return this._objects[objectName]
369
404
  }
370
405
 
406
+ /**
407
+ * Responds to a command from the backend
408
+ * @param {number} commandId
409
+ * @param {any} data
410
+ */
371
411
  respondToCommand(commandId, data) {
372
412
  this.sendCommand("command_response", {command_id: commandId, data})
373
413
  }
374
414
 
415
+ /**
416
+ * Sends a command to the backend and returns a promise that resolves with the response
417
+ * @param {string} command
418
+ * @param {any} data
419
+ * @returns {Promise<any>}
420
+ */
375
421
  sendCommand(command, data) {
376
422
  return new Promise((resolve, reject) => {
377
423
  const outgoingCommandCount = ++this.outgoingCommandsCount
@@ -387,6 +433,10 @@ export default class Client {
387
433
  })
388
434
  }
389
435
 
436
+ /**
437
+ * Sends data to the backend
438
+ * @param {any} data
439
+ */
390
440
  send(data) {
391
441
  this.backend.send(data)
392
442
  }
@@ -407,7 +457,7 @@ export default class Client {
407
457
  * Spawns a new reference to an object
408
458
  *
409
459
  * @param {string} id
410
- * @returns {Promise<Reference>}
460
+ * @returns {Reference}
411
461
  */
412
462
  spawnReference(id) {
413
463
  const reference = new Reference(this, id)
@@ -1,6 +1,18 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @param {import("./reference.js").default} reference
5
+ * @param {string} prop
6
+ * @returns {(...args: any[]) => Promise<any>}
7
+ */
1
8
  const proxyMethodSpawner = (reference, prop) => (...args) => reference.callMethodWithReference(prop, ...args)
2
9
 
3
10
  const proxyObjectHandler = {
11
+ /**
12
+ * @param {import("./reference.js").default|(() => import("./reference.js").default)} reference
13
+ * @param {string} prop
14
+ * @returns {any}
15
+ */
4
16
  get(reference, prop) {
5
17
  if (typeof reference == "function") reference = reference()
6
18
 
@@ -14,16 +26,28 @@ const proxyObjectHandler = {
14
26
  return proxyMethodSpawner(reference, prop)
15
27
  },
16
28
 
29
+ /**
30
+ * @param {import("./reference.js").default|(() => import("./reference.js").default)} receiver
31
+ * @param {string} prop
32
+ * @param {any} newValue
33
+ */
17
34
  set(receiver, prop, newValue) {
18
35
  throw new Error("set property isn't supported yet")
19
36
 
20
- if (typeof receiver == "function") receiver = receiver()
21
- if (!(prop in receiver)) throw new PropertyNotFoundError(`Property not found: ${prop}`)
37
+ // @ts-expect-error
38
+ if (typeof receiver == "function") receiver = receiver() // eslint-disable-line no-unreachable
39
+
40
+ // @ts-expect-error
41
+ if (!(prop in receiver)) throw new PropertyNotFoundError(`Property not found: ${prop}`) // eslint-disable-line no-undef
22
42
 
23
43
  return Reflect.set(receiver, prop, newValue)
24
44
  }
25
45
  }
26
46
 
47
+ /**
48
+ * @param {any} wrappedObject
49
+ * @returns {Proxy}
50
+ */
27
51
  const referenceProxy = (wrappedObject) => new Proxy(wrappedObject, proxyObjectHandler)
28
52
 
29
53
  export default referenceProxy
@@ -1,3 +1,5 @@
1
+ // @ts-check
2
+
1
3
  export default class Reference {
2
4
  /**
3
5
  * Creates a new Reference
package/src/logger.js CHANGED
@@ -1,3 +1,5 @@
1
+ // @ts-check
2
+
1
3
  export default class Logger {
2
4
  /**
3
5
  * Creates a new Logger instance
@@ -29,13 +31,25 @@ export default class Logger {
29
31
 
30
32
  /**
31
33
  * Logs a message to the console if debug is enabled
32
- *
33
34
  * @param {...any} args
34
35
  */
35
36
  log(...args) {
36
37
  return this._sendToConsole("log", ...args)
37
38
  }
38
39
 
40
+ /**
41
+ * Logs a warning message to the console if debug is enabled
42
+ * @param {...any} args
43
+ */
44
+ warn(...args) {
45
+ return this._sendToConsole("warn", ...args)
46
+ }
47
+
48
+ /**
49
+ * Sends the log message to the console
50
+ * @param {string} logType
51
+ * @param {...any} args
52
+ */
39
53
  _sendToConsole(logType, ...args) {
40
54
  if (!this.debug) {
41
55
  return
@@ -1,12 +1,23 @@
1
+ // @ts-check
2
+
1
3
  import {exec, spawn} from "child_process"
2
4
  import Logger from "./logger.js"
3
- import {realpath} from "node:fs/promises"
5
+ import {realpath} from "fs/promises"
4
6
 
5
7
  const logger = new Logger("Scoundrel PythonWebSocketRunner")
6
8
 
7
9
  // logger.setDebug(true)
8
10
 
9
11
  export default class PythonWebSocketRunner {
12
+ /** @type {Error | null} */
13
+ waitForPidRejectError = null
14
+
15
+ /** @type {((value: any) => void) | null} */
16
+ waitForPidResolve = null
17
+
18
+ /** @type {((value: any) => void) | null} */
19
+ waitForPidReject = null
20
+
10
21
  constructor() {
11
22
  process.on("exit", this.onProcessExit)
12
23
  }
@@ -36,10 +47,18 @@ export default class PythonWebSocketRunner {
36
47
  }
37
48
  }
38
49
 
50
+ /**
51
+ * @param {number} code
52
+ * @param {string} signal
53
+ */
39
54
  onChildExit = (code, signal) => {
40
55
  logger.log(() => `Child process exited with code ${code} and signal ${signal}`)
41
56
 
42
57
  if (this.waitForPidRejectError) {
58
+ if (!this.waitForPidReject) {
59
+ throw new Error("waitForPidReject is undefined while waitForPidRejectError is set")
60
+ }
61
+
43
62
  this.waitForPidReject(this.waitForPidRejectError)
44
63
  this.waitForPidResolve = null
45
64
  this.waitForPidReject = null
@@ -52,6 +71,9 @@ export default class PythonWebSocketRunner {
52
71
  }
53
72
  }
54
73
 
74
+ /**
75
+ * @param {string | Buffer} data
76
+ */
55
77
  onChildStderr = (data) => {
56
78
  logger.error(() => `stderr: ${data}`)
57
79
 
@@ -60,6 +82,9 @@ export default class PythonWebSocketRunner {
60
82
  }
61
83
  }
62
84
 
85
+ /**
86
+ * @param {string | Buffer} data
87
+ */
63
88
  onChildStdout = (data) => {
64
89
  logger.log(() => `stdout: ${data}`)
65
90
 
@@ -71,7 +96,11 @@ export default class PythonWebSocketRunner {
71
96
  logger.log(() => `Registered PID ${this.pid}`)
72
97
 
73
98
  if (this.waitForPidResolve) {
74
- this.waitForPidResolve()
99
+ if (!this.waitForPidResolve) {
100
+ throw new Error("waitForPidResolve is undefined while waitForPidRejectError is set")
101
+ }
102
+
103
+ this.waitForPidResolve(null)
75
104
  this.waitForPidResolve = null
76
105
  this.waitForPidReject = null
77
106
  this.waitForPidRejectError = null
@@ -1,4 +1,10 @@
1
+ // @ts-check
2
+
1
3
  export default class WebSocketClient {
4
+ /**
5
+ * Creates a new WebSocketClient
6
+ * @param {import("ws").WebSocket} ws The WebSocket instance
7
+ */
2
8
  constructor(ws) {
3
9
  this.ws = ws
4
10
 
@@ -6,14 +12,23 @@ export default class WebSocketClient {
6
12
  ws.on("message", this.onMessage)
7
13
  }
8
14
 
15
+ /**
16
+ * @param {(data: any) => void} callback
17
+ */
9
18
  onCommand(callback) {
10
19
  this.onCommandCallback = callback
11
20
  }
12
21
 
22
+ /**
23
+ * @param {Error} error
24
+ */
13
25
  onError = (error) => {
14
26
  console.error("WebSocketClient error", error)
15
27
  }
16
28
 
29
+ /**
30
+ * @param {string} rawData
31
+ */
17
32
  onMessage = (rawData) => {
18
33
  const data = JSON.parse(rawData)
19
34
 
@@ -22,6 +37,9 @@ export default class WebSocketClient {
22
37
  this.onCommandCallback(data)
23
38
  }
24
39
 
40
+ /**
41
+ * @param {any} data
42
+ */
25
43
  async send(data) {
26
44
  await this.ws.send(JSON.stringify(data))
27
45
  }
@@ -1,6 +1,12 @@
1
+ // @ts-check
2
+
1
3
  import WebSocketClient from "./client.js"
2
4
 
3
5
  export default class WebSocket {
6
+ /**
7
+ * Creates a new WebSocket connection handler
8
+ * @param {import("ws").Server} webSocketServer The WebSocket server instance
9
+ */
4
10
  constructor(webSocketServer) {
5
11
  this.wss = webSocketServer
6
12
  this.wss.on("connection", this.onConnection)
@@ -8,12 +14,18 @@ export default class WebSocket {
8
14
 
9
15
  close() { this.wss.close() }
10
16
 
17
+ /**
18
+ * @param {import("ws").WebSocket} ws
19
+ */
11
20
  onConnection = (ws) => {
12
21
  if (!this.onNewClientCallback) throw new Error("'onNewClient' hasn't been called")
13
22
 
14
23
  this.onNewClientCallback(new WebSocketClient(ws))
15
24
  }
16
25
 
26
+ /**
27
+ * @param {(client: import("./client.js").default) => void} callback
28
+ */
17
29
  onNewClient(callback) {
18
30
  if (!callback) throw new Error("No callback was given")
19
31
 
@@ -1,17 +1,29 @@
1
+ // @ts-check
2
+
1
3
  import Client from "../client/index.js"
2
4
  import EventEmitter from "events"
3
5
 
4
6
  export default class ScoundrelServer {
7
+ /**
8
+ * Creates a new Scoundrel server
9
+ * @param {import("./connections/web-socket/index.js").default} backend The backend connection handler
10
+ */
5
11
  constructor(backend) {
6
12
  this.backend = backend
7
13
  this.backend.onNewClient(this.onNewClient)
14
+
15
+ /** @type {Client[]} */
8
16
  this.clients = []
17
+
9
18
  this.events = new EventEmitter()
10
19
  }
11
20
 
12
21
  close() { this.backend.close() }
13
22
  getClients() { return this.clients }
14
23
 
24
+ /**
25
+ * @param {import("./connections/web-socket/client.js").default} clientBackend
26
+ */
15
27
  onNewClient = (clientBackend) => {
16
28
  const client = new Client(clientBackend)
17
29
 
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "checkJs": true,
4
+ "noEmit": true,
5
+ "target": "ES2020",
6
+
7
+ "moduleResolution": "nodenext",
8
+ "module": "nodenext",
9
+
10
+ "lib": ["dom", "ES2020"],
11
+ "types": ["jasmine", "node"]
12
+ }
13
+ }