js4j 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +684 -0
- package/examples/01-basic.js +75 -0
- package/examples/02-collections.js +108 -0
- package/examples/03-objects.js +99 -0
- package/examples/04-callbacks.js +97 -0
- package/examples/05-stdlib.js +123 -0
- package/examples/_gateway.js +78 -0
- package/index.d.ts +455 -0
- package/index.js +78 -0
- package/java/TestEntryPoint.java +262 -0
- package/java/py4j.jar +0 -0
- package/package.json +29 -0
- package/src/callbackServer.js +269 -0
- package/src/clientServer.js +81 -0
- package/src/collections.js +325 -0
- package/src/connection.js +253 -0
- package/src/exceptions.js +68 -0
- package/src/gateway.js +222 -0
- package/src/javaObject.js +267 -0
- package/src/jvmView.js +224 -0
- package/src/launcher.js +150 -0
- package/src/protocol.js +413 -0
- package/tests/comparison/run_comparison.js +279 -0
- package/tests/comparison/test_js4j.js +187 -0
- package/tests/comparison/test_py4j.py +171 -0
- package/tests/integration/gateway.integration.test.js +384 -0
- package/tests/unit/collections.test.js +313 -0
- package/tests/unit/exceptions.test.js +104 -0
- package/tests/unit/gateway.test.js +178 -0
- package/tests/unit/jvmView.test.js +126 -0
- package/tests/unit/launcher.test.js +27 -0
- package/tests/unit/protocol.test.js +280 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
LIST_COMMAND_NAME,
|
|
5
|
+
LIST_SORT_SUBCOMMAND_NAME,
|
|
6
|
+
LIST_REVERSE_SUBCOMMAND_NAME,
|
|
7
|
+
LIST_SLICE_SUBCOMMAND_NAME,
|
|
8
|
+
LIST_CONCAT_SUBCOMMAND_NAME,
|
|
9
|
+
LIST_MULT_SUBCOMMAND_NAME,
|
|
10
|
+
LIST_IMULT_SUBCOMMAND_NAME,
|
|
11
|
+
LIST_COUNT_SUBCOMMAND_NAME,
|
|
12
|
+
ARRAY_COMMAND_NAME,
|
|
13
|
+
ARRAY_GET_SUB_COMMAND_NAME,
|
|
14
|
+
ARRAY_SET_SUB_COMMAND_NAME,
|
|
15
|
+
ARRAY_SLICE_SUB_COMMAND_NAME,
|
|
16
|
+
ARRAY_LEN_SUB_COMMAND_NAME,
|
|
17
|
+
CALL_COMMAND_NAME,
|
|
18
|
+
END,
|
|
19
|
+
END_COMMAND_PART,
|
|
20
|
+
INTEGER_TYPE,
|
|
21
|
+
encodeCommandPart,
|
|
22
|
+
decodeReturnValue,
|
|
23
|
+
} = require('./protocol');
|
|
24
|
+
|
|
25
|
+
function buildListCommand(subCmd, targetId, ...parts) {
|
|
26
|
+
let cmd = LIST_COMMAND_NAME + subCmd + targetId + END_COMMAND_PART;
|
|
27
|
+
for (const p of parts) { cmd += p; }
|
|
28
|
+
cmd += END + END_COMMAND_PART;
|
|
29
|
+
return cmd;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildCallCommand(targetId, method, args, gatewayClient) {
|
|
33
|
+
const { buildArgsCommand } = require('./javaObject');
|
|
34
|
+
const argsStr = buildArgsCommand(args, gatewayClient._proxyPool);
|
|
35
|
+
return (
|
|
36
|
+
CALL_COMMAND_NAME +
|
|
37
|
+
targetId + END_COMMAND_PART +
|
|
38
|
+
method + END_COMMAND_PART +
|
|
39
|
+
argsStr +
|
|
40
|
+
END + END_COMMAND_PART
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// JavaList — wraps java.util.List
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
function createJavaList(targetId, gatewayClient) {
|
|
49
|
+
const obj = {
|
|
50
|
+
_targetId: targetId,
|
|
51
|
+
_gatewayClient: gatewayClient,
|
|
52
|
+
_isJavaList: true,
|
|
53
|
+
|
|
54
|
+
async size() { return gatewayClient.callMethod(targetId, 'size', []); },
|
|
55
|
+
async get(index) { return gatewayClient.callMethod(targetId, 'get', [index]); },
|
|
56
|
+
async add(element) { return gatewayClient.callMethod(targetId, 'add', [element]); },
|
|
57
|
+
async addAt(index, element) { return gatewayClient.callMethod(targetId, 'add', [index, element]); },
|
|
58
|
+
async remove(indexOrValue) { return gatewayClient.callMethod(targetId, 'remove', [indexOrValue]); },
|
|
59
|
+
async set(index, element) { return gatewayClient.callMethod(targetId, 'set', [index, element]); },
|
|
60
|
+
async clear() { return gatewayClient.callMethod(targetId, 'clear', []); },
|
|
61
|
+
async contains(value) { return gatewayClient.callMethod(targetId, 'contains', [value]); },
|
|
62
|
+
async indexOf(value) { return gatewayClient.callMethod(targetId, 'indexOf', [value]); },
|
|
63
|
+
|
|
64
|
+
async subList(fromIndex, toIndex) {
|
|
65
|
+
const answer = await gatewayClient._sendCommand(
|
|
66
|
+
buildListCommand(
|
|
67
|
+
LIST_SLICE_SUBCOMMAND_NAME, targetId,
|
|
68
|
+
INTEGER_TYPE + fromIndex + END_COMMAND_PART,
|
|
69
|
+
INTEGER_TYPE + toIndex + END_COMMAND_PART
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
return decodeReturnValue(answer, gatewayClient);
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
async sort() {
|
|
76
|
+
const answer = await gatewayClient._sendCommand(buildListCommand(LIST_SORT_SUBCOMMAND_NAME, targetId));
|
|
77
|
+
return decodeReturnValue(answer, gatewayClient);
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
async reverse() {
|
|
81
|
+
const answer = await gatewayClient._sendCommand(buildListCommand(LIST_REVERSE_SUBCOMMAND_NAME, targetId));
|
|
82
|
+
return decodeReturnValue(answer, gatewayClient);
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
async count(value) {
|
|
86
|
+
const answer = await gatewayClient._sendCommand(
|
|
87
|
+
buildListCommand(LIST_COUNT_SUBCOMMAND_NAME, targetId, encodeCommandPart(value, gatewayClient._proxyPool))
|
|
88
|
+
);
|
|
89
|
+
return decodeReturnValue(answer, gatewayClient);
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
async *[Symbol.asyncIterator]() {
|
|
93
|
+
const size = await this.size();
|
|
94
|
+
for (let i = 0; i < size; i++) { yield await this.get(i); }
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
async toArray() {
|
|
98
|
+
const size = await this.size();
|
|
99
|
+
const result = [];
|
|
100
|
+
for (let i = 0; i < size; i++) { result.push(await this.get(i)); }
|
|
101
|
+
return result;
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
async call(method, ...args) { return gatewayClient.callMethod(targetId, method, args); },
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return new Proxy(obj, {
|
|
108
|
+
get(target, prop) {
|
|
109
|
+
if (typeof prop === 'symbol') return target[prop];
|
|
110
|
+
if (Object.prototype.hasOwnProperty.call(target, prop)) return target[prop];
|
|
111
|
+
if (prop === 'then') return undefined;
|
|
112
|
+
return (...args) => gatewayClient.callMethod(targetId, prop, args);
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// JavaSet — wraps java.util.Set
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
function createJavaSet(targetId, gatewayClient) {
|
|
122
|
+
const obj = {
|
|
123
|
+
_targetId: targetId,
|
|
124
|
+
_gatewayClient: gatewayClient,
|
|
125
|
+
_isJavaSet: true,
|
|
126
|
+
|
|
127
|
+
async size() { return gatewayClient.callMethod(targetId, 'size', []); },
|
|
128
|
+
async add(element) { return gatewayClient.callMethod(targetId, 'add', [element]); },
|
|
129
|
+
async remove(element) { return gatewayClient.callMethod(targetId, 'remove', [element]); },
|
|
130
|
+
async contains(element) { return gatewayClient.callMethod(targetId, 'contains', [element]); },
|
|
131
|
+
async clear() { return gatewayClient.callMethod(targetId, 'clear', []); },
|
|
132
|
+
|
|
133
|
+
async toArray() {
|
|
134
|
+
const iter = await gatewayClient.callMethod(targetId, 'iterator', []);
|
|
135
|
+
const result = [];
|
|
136
|
+
while (await iter.hasNext()) { result.push(await iter.next()); }
|
|
137
|
+
return result;
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
async toSet() {
|
|
141
|
+
const iter = await gatewayClient.callMethod(targetId, 'iterator', []);
|
|
142
|
+
const result = new Set();
|
|
143
|
+
while (await iter.hasNext()) { result.add(await iter.next()); }
|
|
144
|
+
return result;
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
async *[Symbol.asyncIterator]() {
|
|
148
|
+
const iter = await gatewayClient.callMethod(targetId, 'iterator', []);
|
|
149
|
+
while (await iter.hasNext()) { yield await iter.next(); }
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
async call(method, ...args) { return gatewayClient.callMethod(targetId, method, args); },
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return new Proxy(obj, {
|
|
156
|
+
get(target, prop) {
|
|
157
|
+
if (typeof prop === 'symbol') return target[prop];
|
|
158
|
+
if (Object.prototype.hasOwnProperty.call(target, prop)) return target[prop];
|
|
159
|
+
if (prop === 'then') return undefined;
|
|
160
|
+
return (...args) => gatewayClient.callMethod(targetId, prop, args);
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// JavaMap — wraps java.util.Map
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
function createJavaMap(targetId, gatewayClient) {
|
|
170
|
+
const obj = {
|
|
171
|
+
_targetId: targetId,
|
|
172
|
+
_gatewayClient: gatewayClient,
|
|
173
|
+
_isJavaMap: true,
|
|
174
|
+
|
|
175
|
+
async size() { return gatewayClient.callMethod(targetId, 'size', []); },
|
|
176
|
+
async get(key) { return gatewayClient.callMethod(targetId, 'get', [key]); },
|
|
177
|
+
async put(key, value) { return gatewayClient.callMethod(targetId, 'put', [key, value]); },
|
|
178
|
+
async remove(key) { return gatewayClient.callMethod(targetId, 'remove', [key]); },
|
|
179
|
+
async containsKey(key) { return gatewayClient.callMethod(targetId, 'containsKey', [key]); },
|
|
180
|
+
async containsValue(value) { return gatewayClient.callMethod(targetId, 'containsValue', [value]); },
|
|
181
|
+
async clear() { return gatewayClient.callMethod(targetId, 'clear', []); },
|
|
182
|
+
async keySet() { return gatewayClient.callMethod(targetId, 'keySet', []); },
|
|
183
|
+
async values() { return gatewayClient.callMethod(targetId, 'values', []); },
|
|
184
|
+
async entrySet() { return gatewayClient.callMethod(targetId, 'entrySet', []); },
|
|
185
|
+
|
|
186
|
+
async toMap() {
|
|
187
|
+
const result = new Map();
|
|
188
|
+
const entries = await this.entrySet();
|
|
189
|
+
for await (const entry of entries) {
|
|
190
|
+
const key = await entry.getKey();
|
|
191
|
+
const val = await entry.getValue();
|
|
192
|
+
result.set(key, val);
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
async toObject() {
|
|
198
|
+
const result = {};
|
|
199
|
+
const keys = await this.keySet();
|
|
200
|
+
for await (const key of keys) { result[key] = await this.get(key); }
|
|
201
|
+
return result;
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
async call(method, ...args) { return gatewayClient.callMethod(targetId, method, args); },
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
return new Proxy(obj, {
|
|
208
|
+
get(target, prop) {
|
|
209
|
+
if (typeof prop === 'symbol') return target[prop];
|
|
210
|
+
if (Object.prototype.hasOwnProperty.call(target, prop)) return target[prop];
|
|
211
|
+
if (prop === 'then') return undefined;
|
|
212
|
+
return (...args) => gatewayClient.callMethod(targetId, prop, args);
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// JavaArray — wraps Java arrays (fixed length)
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
function createJavaArray(targetId, gatewayClient) {
|
|
222
|
+
function arrayCmd(subCmd, ...parts) {
|
|
223
|
+
let cmd = ARRAY_COMMAND_NAME + subCmd + targetId + END_COMMAND_PART;
|
|
224
|
+
for (const p of parts) cmd += p;
|
|
225
|
+
cmd += END + END_COMMAND_PART;
|
|
226
|
+
return cmd;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const obj = {
|
|
230
|
+
_targetId: targetId,
|
|
231
|
+
_gatewayClient: gatewayClient,
|
|
232
|
+
_isJavaArray: true,
|
|
233
|
+
|
|
234
|
+
async get(index) {
|
|
235
|
+
const answer = await gatewayClient._sendCommand(
|
|
236
|
+
arrayCmd(ARRAY_GET_SUB_COMMAND_NAME, INTEGER_TYPE + index + END_COMMAND_PART)
|
|
237
|
+
);
|
|
238
|
+
return decodeReturnValue(answer, gatewayClient);
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
async set(index, value) {
|
|
242
|
+
const answer = await gatewayClient._sendCommand(
|
|
243
|
+
arrayCmd(ARRAY_SET_SUB_COMMAND_NAME,
|
|
244
|
+
INTEGER_TYPE + index + END_COMMAND_PART,
|
|
245
|
+
encodeCommandPart(value, gatewayClient._proxyPool))
|
|
246
|
+
);
|
|
247
|
+
return decodeReturnValue(answer, gatewayClient);
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
async length() {
|
|
251
|
+
const answer = await gatewayClient._sendCommand(arrayCmd(ARRAY_LEN_SUB_COMMAND_NAME));
|
|
252
|
+
return decodeReturnValue(answer, gatewayClient);
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
async slice(fromIndex, toIndex) {
|
|
256
|
+
const answer = await gatewayClient._sendCommand(
|
|
257
|
+
arrayCmd(ARRAY_SLICE_SUB_COMMAND_NAME,
|
|
258
|
+
INTEGER_TYPE + fromIndex + END_COMMAND_PART,
|
|
259
|
+
INTEGER_TYPE + toIndex + END_COMMAND_PART)
|
|
260
|
+
);
|
|
261
|
+
return decodeReturnValue(answer, gatewayClient);
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
async toArray() {
|
|
265
|
+
const len = await this.length();
|
|
266
|
+
const result = [];
|
|
267
|
+
for (let i = 0; i < len; i++) { result.push(await this.get(i)); }
|
|
268
|
+
return result;
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
async *[Symbol.asyncIterator]() {
|
|
272
|
+
const len = await this.length();
|
|
273
|
+
for (let i = 0; i < len; i++) { yield await this.get(i); }
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
async call(method, ...args) { return gatewayClient.callMethod(targetId, method, args); },
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
return obj;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// JavaIterator — wraps java.util.Iterator
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
function createJavaIterator(targetId, gatewayClient) {
|
|
287
|
+
const obj = {
|
|
288
|
+
_targetId: targetId,
|
|
289
|
+
_gatewayClient: gatewayClient,
|
|
290
|
+
_isJavaIterator: true,
|
|
291
|
+
|
|
292
|
+
async hasNext() { return gatewayClient.callMethod(targetId, 'hasNext', []); },
|
|
293
|
+
async next() { return gatewayClient.callMethod(targetId, 'next', []); },
|
|
294
|
+
async remove() { return gatewayClient.callMethod(targetId, 'remove', []); },
|
|
295
|
+
|
|
296
|
+
async *[Symbol.asyncIterator]() {
|
|
297
|
+
while (await this.hasNext()) { yield await this.next(); }
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
async toArray() {
|
|
301
|
+
const result = [];
|
|
302
|
+
while (await this.hasNext()) { result.push(await this.next()); }
|
|
303
|
+
return result;
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
async call(method, ...args) { return gatewayClient.callMethod(targetId, method, args); },
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
return new Proxy(obj, {
|
|
310
|
+
get(target, prop) {
|
|
311
|
+
if (typeof prop === 'symbol') return target[prop];
|
|
312
|
+
if (Object.prototype.hasOwnProperty.call(target, prop)) return target[prop];
|
|
313
|
+
if (prop === 'then') return undefined;
|
|
314
|
+
return (...args) => gatewayClient.callMethod(targetId, prop, args);
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
module.exports = {
|
|
320
|
+
createJavaList,
|
|
321
|
+
createJavaSet,
|
|
322
|
+
createJavaMap,
|
|
323
|
+
createJavaArray,
|
|
324
|
+
createJavaIterator,
|
|
325
|
+
};
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const net = require('net');
|
|
4
|
+
const { Js4JNetworkError, Js4JAuthenticationError } = require('./exceptions');
|
|
5
|
+
const {
|
|
6
|
+
AUTH_COMMAND_NAME,
|
|
7
|
+
END,
|
|
8
|
+
END_COMMAND_PART,
|
|
9
|
+
SUCCESS,
|
|
10
|
+
} = require('./protocol');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Serialises concurrent requests so that only one in-flight command exists on
|
|
14
|
+
* the socket at a time — matching py4j's synchronous request/response model.
|
|
15
|
+
*/
|
|
16
|
+
class RequestQueue {
|
|
17
|
+
constructor() {
|
|
18
|
+
this._queue = [];
|
|
19
|
+
this._running = false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Enqueue an async task function and return its result as a Promise. */
|
|
23
|
+
run(task) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
this._queue.push({ task, resolve, reject });
|
|
26
|
+
this._flush();
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async _flush() {
|
|
31
|
+
if (this._running) return;
|
|
32
|
+
this._running = true;
|
|
33
|
+
while (this._queue.length > 0) {
|
|
34
|
+
const { task, resolve, reject } = this._queue.shift();
|
|
35
|
+
try {
|
|
36
|
+
resolve(await task());
|
|
37
|
+
} catch (err) {
|
|
38
|
+
reject(err);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
this._running = false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A single TCP connection to a py4j-compatible GatewayServer.
|
|
47
|
+
*
|
|
48
|
+
* Handles buffered line-by-line reading and serialises commands through a
|
|
49
|
+
* RequestQueue so the socket is never used concurrently.
|
|
50
|
+
*/
|
|
51
|
+
class GatewayConnection {
|
|
52
|
+
constructor(options = {}) {
|
|
53
|
+
this.host = options.host || '127.0.0.1';
|
|
54
|
+
this.port = options.port || 25333;
|
|
55
|
+
this.authToken = options.authToken || null;
|
|
56
|
+
this._socket = null;
|
|
57
|
+
this._buffer = '';
|
|
58
|
+
this._readResolve = null;
|
|
59
|
+
this._readReject = null;
|
|
60
|
+
this._queue = new RequestQueue();
|
|
61
|
+
this._closed = false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Open the TCP connection (and optionally authenticate). */
|
|
65
|
+
connect() {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const sock = new net.Socket();
|
|
68
|
+
sock.setEncoding('utf8');
|
|
69
|
+
sock.setNoDelay(true);
|
|
70
|
+
|
|
71
|
+
sock.on('data', (chunk) => {
|
|
72
|
+
this._buffer += chunk;
|
|
73
|
+
this._drainBuffer();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
sock.on('error', (err) => {
|
|
77
|
+
this._closed = true;
|
|
78
|
+
if (this._readReject) {
|
|
79
|
+
this._readReject(new Js4JNetworkError('Socket error: ' + err.message));
|
|
80
|
+
this._readReject = null;
|
|
81
|
+
this._readResolve = null;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
sock.on('close', () => {
|
|
86
|
+
this._closed = true;
|
|
87
|
+
if (this._readReject) {
|
|
88
|
+
this._readReject(new Js4JNetworkError('Connection closed by remote host'));
|
|
89
|
+
this._readReject = null;
|
|
90
|
+
this._readResolve = null;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
sock.connect(this.port, this.host, async () => {
|
|
95
|
+
this._socket = sock;
|
|
96
|
+
try {
|
|
97
|
+
if (this.authToken) {
|
|
98
|
+
await this._authenticate();
|
|
99
|
+
}
|
|
100
|
+
resolve(this);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
sock.destroy();
|
|
103
|
+
reject(err);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
sock.on('error', (err) => {
|
|
108
|
+
if (!this._socket) {
|
|
109
|
+
// Connection-phase error
|
|
110
|
+
reject(new Js4JNetworkError('Could not connect to gateway: ' + err.message));
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Send an authentication command and verify the response. */
|
|
117
|
+
async _authenticate() {
|
|
118
|
+
const cmd = AUTH_COMMAND_NAME + this.authToken + END_COMMAND_PART + END + END_COMMAND_PART;
|
|
119
|
+
const resp = await this._rawSend(cmd);
|
|
120
|
+
if (!resp.startsWith(SUCCESS)) {
|
|
121
|
+
throw new Js4JAuthenticationError('Gateway authentication failed');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Send a py4j command string and return the single-line response.
|
|
127
|
+
* Requests are queued; only one is in flight at a time.
|
|
128
|
+
*/
|
|
129
|
+
sendCommand(command) {
|
|
130
|
+
if (this._closed) {
|
|
131
|
+
return Promise.reject(new Js4JNetworkError('Connection is closed'));
|
|
132
|
+
}
|
|
133
|
+
return this._queue.run(() => this._rawSend(command));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Internal: write to socket and wait for one line of response. */
|
|
137
|
+
_rawSend(command) {
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
this._readResolve = resolve;
|
|
140
|
+
this._readReject = reject;
|
|
141
|
+
this._socket.write(command, 'utf8', (err) => {
|
|
142
|
+
if (err) {
|
|
143
|
+
this._readResolve = null;
|
|
144
|
+
this._readReject = null;
|
|
145
|
+
reject(new Js4JNetworkError('Write failed: ' + err.message));
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Drain the internal buffer, resolving the pending read on a complete line. */
|
|
152
|
+
_drainBuffer() {
|
|
153
|
+
const idx = this._buffer.indexOf('\n');
|
|
154
|
+
if (idx !== -1 && this._readResolve) {
|
|
155
|
+
const line = this._buffer.substring(0, idx);
|
|
156
|
+
this._buffer = this._buffer.substring(idx + 1);
|
|
157
|
+
const resolve = this._readResolve;
|
|
158
|
+
this._readResolve = null;
|
|
159
|
+
this._readReject = null;
|
|
160
|
+
resolve(line);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Close this connection. */
|
|
165
|
+
close() {
|
|
166
|
+
this._closed = true;
|
|
167
|
+
if (this._socket) {
|
|
168
|
+
this._socket.destroy();
|
|
169
|
+
this._socket = null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
get isConnected() {
|
|
174
|
+
return !this._closed && this._socket !== null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* A pool of GatewayConnections.
|
|
180
|
+
* py4j gives each thread its own connection; in Node.js we hand out
|
|
181
|
+
* connections from the pool (creating new ones as needed) and return them
|
|
182
|
+
* when the request is done.
|
|
183
|
+
*
|
|
184
|
+
* For simplicity the default pool size is 1 (sequential behaviour matching
|
|
185
|
+
* py4j's single-threaded usage), but you can raise maxConnections for
|
|
186
|
+
* concurrent callers.
|
|
187
|
+
*/
|
|
188
|
+
class ConnectionPool {
|
|
189
|
+
constructor(options = {}, maxConnections = 4) {
|
|
190
|
+
this._options = options;
|
|
191
|
+
this._maxConnections = maxConnections;
|
|
192
|
+
this._idle = []; // available connections
|
|
193
|
+
this._active = 0;
|
|
194
|
+
this._waiters = [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Borrow a connection from the pool (creates one if needed). */
|
|
198
|
+
async acquire() {
|
|
199
|
+
if (this._idle.length > 0) {
|
|
200
|
+
const conn = this._idle.pop();
|
|
201
|
+
if (conn.isConnected) {
|
|
202
|
+
this._active++;
|
|
203
|
+
return conn;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (this._active < this._maxConnections) {
|
|
208
|
+
const conn = new GatewayConnection(this._options);
|
|
209
|
+
await conn.connect();
|
|
210
|
+
this._active++;
|
|
211
|
+
return conn;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Wait for a connection to become available
|
|
215
|
+
return new Promise((resolve, reject) => {
|
|
216
|
+
this._waiters.push({ resolve, reject });
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Return a connection to the pool. */
|
|
221
|
+
release(conn) {
|
|
222
|
+
this._active--;
|
|
223
|
+
if (this._waiters.length > 0) {
|
|
224
|
+
const { resolve } = this._waiters.shift();
|
|
225
|
+
this._active++;
|
|
226
|
+
resolve(conn);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (conn.isConnected) {
|
|
230
|
+
this._idle.push(conn);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Run a function with a borrowed connection, then release it. */
|
|
235
|
+
async withConnection(fn) {
|
|
236
|
+
const conn = await this.acquire();
|
|
237
|
+
try {
|
|
238
|
+
return await fn(conn);
|
|
239
|
+
} finally {
|
|
240
|
+
this.release(conn);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Close all connections in the pool. */
|
|
245
|
+
closeAll() {
|
|
246
|
+
for (const conn of this._idle) {
|
|
247
|
+
conn.close();
|
|
248
|
+
}
|
|
249
|
+
this._idle = [];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = { GatewayConnection, ConnectionPool, RequestQueue };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base error class for all js4j errors.
|
|
5
|
+
* Mirrors py4j's Py4JError.
|
|
6
|
+
*/
|
|
7
|
+
class Js4JError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'Js4JError';
|
|
11
|
+
if (Error.captureStackTrace) {
|
|
12
|
+
Error.captureStackTrace(this, this.constructor);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Raised when a Java-side exception occurs during method invocation.
|
|
19
|
+
* Contains the full Java exception message and optional stack trace.
|
|
20
|
+
* Mirrors py4j's Py4JJavaError.
|
|
21
|
+
*/
|
|
22
|
+
class Js4JJavaError extends Js4JError {
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} message - Human-readable error message
|
|
25
|
+
* @param {string} javaExceptionMessage - Raw protocol payload (e.g. "ro0")
|
|
26
|
+
* @param {object} [javaException] - Decoded Java Throwable as a JavaObject;
|
|
27
|
+
* call await err.javaException.getMessage() etc.
|
|
28
|
+
*/
|
|
29
|
+
constructor(message, javaExceptionMessage, javaException) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = 'Js4JJavaError';
|
|
32
|
+
this.javaExceptionMessage = javaExceptionMessage || '';
|
|
33
|
+
this.javaException = javaException || null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
toString() {
|
|
37
|
+
return `${this.name}: ${this.message}\n Java exception payload: ${this.javaExceptionMessage}`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Raised when a network error occurs during communication with the Java gateway.
|
|
43
|
+
* Mirrors py4j's Py4JNetworkError.
|
|
44
|
+
*/
|
|
45
|
+
class Js4JNetworkError extends Js4JError {
|
|
46
|
+
constructor(message) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = 'Js4JNetworkError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Raised when authentication with the Java gateway fails.
|
|
54
|
+
* Mirrors py4j's Py4JAuthenticationError.
|
|
55
|
+
*/
|
|
56
|
+
class Js4JAuthenticationError extends Js4JError {
|
|
57
|
+
constructor(message) {
|
|
58
|
+
super(message || 'Authentication failed');
|
|
59
|
+
this.name = 'Js4JAuthenticationError';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
Js4JError,
|
|
65
|
+
Js4JJavaError,
|
|
66
|
+
Js4JNetworkError,
|
|
67
|
+
Js4JAuthenticationError,
|
|
68
|
+
};
|