Skip to content

Commit 94237b5

Browse files
abogoyavlenskyavli
authored andcommitted
Add ability to show large evaluation result
1 parent bbd12ac commit 94237b5

File tree

3 files changed

+208
-42
lines changed

3 files changed

+208
-42
lines changed

src/bencodeUtil.ts

+69-29
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,87 @@
11
import * as bencoder from 'bencoder';
22

3-
const CONTINUATION_ERROR_MESSAGE: string = "Unexpected continuation: \"";
4-
53
interface DecodedResult {
64
decodedObjects: any[];
75
rest: Buffer;
6+
isDone: boolean;
7+
}
8+
9+
interface Message {
10+
msg: any;
11+
buffer: Buffer;
12+
msgLen: number;
813
}
914

15+
const CONTINUATION_ERROR_MESSAGE: string = "Unexpected continuation: \"";
16+
const BENCODE_END_SYMBOL = 0x65; // `e`
17+
const VALUE_LENGTH_REGEXP = /\:(?<name>value|out)(?<len>\d+)\:/m;
18+
1019
export function encode(msg: any): Buffer {
1120
return bencoder.encode(msg);
1221
}
1322

23+
function isMessageIncomplete(message: Message): boolean {
24+
const lastByte = message.buffer[message.buffer.length - 1],
25+
matched = message.buffer.toString().match(VALUE_LENGTH_REGEXP),
26+
// @ts-ignore: target es6 doesn't support `groups` in RegExpMatchArray
27+
{ groups: { len, name } = {} } = matched || {},
28+
requiredLength = len ? Number.parseInt(len) : null,
29+
isLengthInvalid = name in message.msg
30+
&& requiredLength !== null
31+
// check length of parsed message
32+
&& message.msg[name].length < requiredLength;
33+
34+
// message's length is valid and the end symbol is presented
35+
return isLengthInvalid || lastByte !== BENCODE_END_SYMBOL;
36+
}
37+
38+
function decodeNextMessage(data: Buffer): Message {
39+
let message: Message = { msg: null, buffer: data, msgLen: data.length };
40+
41+
while (!message.msg) {
42+
try {
43+
message.msg = bencoder.decode(message.buffer.slice(0, message.msgLen));
44+
45+
const isWholeBufferParsed = message.msgLen === message.buffer.length;
46+
if (isWholeBufferParsed && isMessageIncomplete(message)) {
47+
message.msg = null;
48+
break;
49+
}
50+
} catch (error) {
51+
if (!!error.message && error.message.startsWith(CONTINUATION_ERROR_MESSAGE)) {
52+
const unexpectedContinuation: string = error.message.slice(CONTINUATION_ERROR_MESSAGE.length,
53+
error.message.length - 1);
54+
message.msgLen -= unexpectedContinuation.length;
55+
} else {
56+
console.log("Unexpected output decoding error.");
57+
break;
58+
}
59+
}
60+
}
61+
62+
return message;
63+
}
64+
1465
/*
1566
receives a buffer and returns an array of decoded objects and the remaining unused buffer
1667
*/
17-
export function decodeObjects(buffer: Buffer): DecodedResult {
18-
const decodedResult: DecodedResult = { decodedObjects: [], rest: buffer };
19-
return decode(decodedResult);
20-
}
68+
export function decodeBuffer(data: Buffer): DecodedResult {
69+
let result: DecodedResult = { decodedObjects: [], rest: data, isDone: false };
2170

22-
function decode(decodedResult: DecodedResult): DecodedResult {
23-
if (decodedResult.rest.length === 0)
24-
return decodedResult;
25-
26-
try {
27-
const decodedObj = bencoder.decode(decodedResult.rest);
28-
decodedResult.decodedObjects.push(decodedObj);
29-
decodedResult.rest = Buffer.from('');
30-
return decodedResult;
31-
} catch (error) {
32-
const errorMessage: string = error.message;
33-
if (!!errorMessage && errorMessage.startsWith(CONTINUATION_ERROR_MESSAGE)) {
34-
const unexpectedContinuation: string = errorMessage.slice(CONTINUATION_ERROR_MESSAGE.length, errorMessage.length - 1);
35-
36-
const rest = decodedResult.rest;
37-
const encodedObj = rest.slice(0, rest.length - unexpectedContinuation.length);
38-
39-
decodedResult.decodedObjects.push(bencoder.decode(encodedObj));
40-
decodedResult.rest = Buffer.from(unexpectedContinuation);
41-
42-
return decode(decodedResult);
43-
} else {
44-
return decodedResult;
71+
while (result.rest.length > 0) {
72+
const message = decodeNextMessage(result.rest);
73+
if (!message.msg) {
74+
break;
75+
}
76+
77+
result.decodedObjects.push(message.msg);
78+
result.rest = result.rest.slice(message.msgLen, result.rest.length);
79+
80+
if (message.msg.status && message.msg.status.indexOf('done') > -1) {
81+
result.isDone = true;
82+
break;
4583
}
4684
}
85+
86+
return result;
4787
}

src/nreplClient.ts

+4-13
Original file line numberDiff line numberDiff line change
@@ -139,16 +139,11 @@ const send = (msg: Message, connection?: CljConnectionInformation): Promise<any[
139139
const respObjects: any[] = [];
140140
client.on('data', data => {
141141
nreplResp = Buffer.concat([nreplResp, data]);
142-
const { decodedObjects, rest } = bencodeUtil.decodeObjects(nreplResp);
142+
const { decodedObjects, rest, isDone } = bencodeUtil.decodeBuffer(nreplResp);
143143
nreplResp = rest;
144-
const validDecodedObjects = decodedObjects.reduce((objs, obj) => {
145-
if (!isLastNreplObject(objs))
146-
objs.push(obj);
147-
return objs;
148-
}, []);
149-
respObjects.push(...validDecodedObjects);
150-
151-
if (isLastNreplObject(respObjects)) {
144+
respObjects.push(...decodedObjects);
145+
146+
if (isDone) {
152147
client.end();
153148
client.removeAllListeners();
154149
resolve(respObjects);
@@ -157,10 +152,6 @@ const send = (msg: Message, connection?: CljConnectionInformation): Promise<any[
157152
});
158153
};
159154

160-
const isLastNreplObject = (nreplObjects: any[]): boolean => {
161-
const lastObj = [...nreplObjects].pop();
162-
return lastObj && lastObj.status && lastObj.status.indexOf('done') > -1;
163-
}
164155

165156
export const nreplClient = {
166157
complete,

test/bencodeUtil.test.ts

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import * as assert from 'assert';
2+
import { decodeBuffer } from '../src/bencodeUtil';
3+
4+
interface DecodedResult {
5+
decodedObjects: any[];
6+
rest: Buffer;
7+
isDone: boolean;
8+
}
9+
10+
function decodeMessages(messages: string[]): DecodedResult {
11+
let nreplResp = Buffer.from(''),
12+
isDone = false;
13+
const respObjects: any[] = [];
14+
15+
messages.forEach(item => {
16+
nreplResp = Buffer.concat([nreplResp, Buffer.from(item)]);
17+
const response = decodeBuffer(nreplResp);
18+
nreplResp = response.rest;
19+
isDone = response.isDone;
20+
respObjects.push(...response.decodedObjects);
21+
});
22+
23+
return { decodedObjects: respObjects, rest: nreplResp, isDone: isDone };
24+
}
25+
26+
suite('bencodeUtil.decodeBuffer', function () {
27+
test('create new session', () => {
28+
const input = Buffer.from(
29+
'd11:new-session36:58d1e5dc-c717-4864-bf49-e7750ced6f28'
30+
+ '7:session36:7fcd096b-4ee4-4142-bb6b-6fc09e5c41606:statusl4:doneee'),
31+
expected = {
32+
'new-session': '58d1e5dc-c717-4864-bf49-e7750ced6f28',
33+
'session': '7fcd096b-4ee4-4142-bb6b-6fc09e5c4160',
34+
'status': ['done']
35+
},
36+
result = decodeBuffer(input);
37+
assert.ok(result.isDone);
38+
assert.deepEqual(result.decodedObjects, [expected]);
39+
assert.equal(result.rest.length, 0);
40+
});
41+
42+
test('close session', () => {
43+
const input = Buffer.from(
44+
'd7:session36:9968ec29-b87d-4e1f-8444-076280357dd36:statusl4:done14:session-closedee'),
45+
expected = {
46+
'session': '9968ec29-b87d-4e1f-8444-076280357dd3',
47+
'status': ['done', 'session-closed']
48+
},
49+
result = decodeBuffer(input);
50+
assert.ok(result.isDone);
51+
assert.deepEqual(result.decodedObjects, [expected]);
52+
assert.equal(result.rest.length, 0);
53+
});
54+
55+
test('completion candidates', () => {
56+
const input = Buffer.from(
57+
'd11:completionsld9:candidate5:slurp2:ns12:clojure.core4:type8:functioned'
58+
+ '9:candidate14:slingshot.test4:type9:namespaceed9:candidate'
59+
+ '17:slingshot.support4:type9:namespaceed9:candidate19:slingshot.slingshot'
60+
+ '4:type9:namespaceee7:session36:4d32206b-5161-40d2-a4e7-d1be6ec777756:statusl4:doneee'),
61+
expected = {
62+
'session': '4d32206b-5161-40d2-a4e7-d1be6ec77775',
63+
'completions': [
64+
{
65+
'candidate': 'slurp',
66+
'ns': 'clojure.core',
67+
'type': 'function'
68+
},
69+
{
70+
'candidate': 'slingshot.test',
71+
'type': 'namespace'
72+
},
73+
{
74+
'candidate': 'slingshot.support',
75+
'type': 'namespace'
76+
},
77+
{
78+
'candidate': 'slingshot.slingshot',
79+
'type': 'namespace'
80+
},
81+
],
82+
'status': ['done']
83+
},
84+
result = decodeBuffer(input);
85+
assert.ok(result.isDone);
86+
assert.deepEqual(result.decodedObjects, [expected]);
87+
assert.equal(result.rest.length, 0);
88+
});
89+
90+
test('eval simple printing expression', () => {
91+
const messages = [
92+
'd3:out7:"test"\n7:session36:9968ec29-b87d-4e1f-8444-076280357dd3e',
93+
'd7:session36:9968ec29-b87d-4e1f-8444-076280357dd35:value3:niled'
94+
+ '7:session36:9968ec29-b87d-4e1f-8444-076280357dd36:statusl4:doneee'
95+
+ '18:changed-namespacesd13:cheshire.cored7:aliasesd7:factory16:cheshire.factory'
96+
+ '3:gen17:cheshire.generate7:gen-seq21:cheshire.generate-seq5:parse14:cheshire.parsee'
97+
+ '7:internsd11:*generator*de9:*opt-map*de13:copy-arglistsd8:arglists11:([dst'
98+
],
99+
expectedOut = {
100+
'session': '9968ec29-b87d-4e1f-8444-076280357dd3',
101+
'out': '"test"\n',
102+
},
103+
result = decodeMessages(messages);
104+
assert.equal(result.decodedObjects.length, 3);
105+
assert.deepEqual(result.decodedObjects[0], expectedOut);
106+
assert.ok(result.isDone);
107+
assert.notEqual(result.rest.length, 0);
108+
});
109+
110+
test('eval expression with result divided by multiple messages', () => {
111+
const messages = [
112+
'd7:session36:9968ec29-b87d-4e1f-8444-076280357dd35:value184:'
113+
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing e',
114+
'lit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
115+
' Ipsum dolor sit amet consectetur adipiscing elit ut aliquam.e'
116+
+ 'd7:session36:9968ec29-b87d-4e1f-8444-076280357dd36:statusl4:doneee'
117+
],
118+
expectedWithValue = {
119+
'session': '9968ec29-b87d-4e1f-8444-076280357dd3',
120+
'value': 'Lorem ipsum dolor sit amet, consectetur adipiscing e'
121+
+ 'lit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
122+
+ ' Ipsum dolor sit amet consectetur adipiscing elit ut aliquam.'
123+
},
124+
expectedWithDone = {
125+
'session': '9968ec29-b87d-4e1f-8444-076280357dd3',
126+
'status': ['done']
127+
},
128+
result = decodeMessages(messages);
129+
assert.equal(result.decodedObjects.length, 2);
130+
assert.deepEqual(result.decodedObjects[0], expectedWithValue);
131+
assert.deepEqual(result.decodedObjects[1], expectedWithDone);
132+
assert.ok(result.isDone);
133+
assert.equal(result.rest.length, 0);
134+
});
135+
});

0 commit comments

Comments
 (0)