Skip to content

Commit

Permalink
Add ability to show large evaluation result
Browse files Browse the repository at this point in the history
  • Loading branch information
abogoyavlensky authored and avli committed Mar 28, 2020
1 parent bbd12ac commit 94237b5
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 42 deletions.
98 changes: 69 additions & 29 deletions src/bencodeUtil.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,87 @@
import * as bencoder from 'bencoder';

const CONTINUATION_ERROR_MESSAGE: string = "Unexpected continuation: \"";

interface DecodedResult {
decodedObjects: any[];
rest: Buffer;
isDone: boolean;
}

interface Message {
msg: any;
buffer: Buffer;
msgLen: number;
}

const CONTINUATION_ERROR_MESSAGE: string = "Unexpected continuation: \"";
const BENCODE_END_SYMBOL = 0x65; // `e`
const VALUE_LENGTH_REGEXP = /\:(?<name>value|out)(?<len>\d+)\:/m;

export function encode(msg: any): Buffer {
return bencoder.encode(msg);
}

function isMessageIncomplete(message: Message): boolean {
const lastByte = message.buffer[message.buffer.length - 1],
matched = message.buffer.toString().match(VALUE_LENGTH_REGEXP),
// @ts-ignore: target es6 doesn't support `groups` in RegExpMatchArray
{ groups: { len, name } = {} } = matched || {},
requiredLength = len ? Number.parseInt(len) : null,
isLengthInvalid = name in message.msg
&& requiredLength !== null
// check length of parsed message
&& message.msg[name].length < requiredLength;

// message's length is valid and the end symbol is presented
return isLengthInvalid || lastByte !== BENCODE_END_SYMBOL;
}

function decodeNextMessage(data: Buffer): Message {
let message: Message = { msg: null, buffer: data, msgLen: data.length };

while (!message.msg) {
try {
message.msg = bencoder.decode(message.buffer.slice(0, message.msgLen));

const isWholeBufferParsed = message.msgLen === message.buffer.length;
if (isWholeBufferParsed && isMessageIncomplete(message)) {
message.msg = null;
break;
}
} catch (error) {
if (!!error.message && error.message.startsWith(CONTINUATION_ERROR_MESSAGE)) {
const unexpectedContinuation: string = error.message.slice(CONTINUATION_ERROR_MESSAGE.length,
error.message.length - 1);
message.msgLen -= unexpectedContinuation.length;
} else {
console.log("Unexpected output decoding error.");
break;
}
}
}

return message;
}

/*
receives a buffer and returns an array of decoded objects and the remaining unused buffer
*/
export function decodeObjects(buffer: Buffer): DecodedResult {
const decodedResult: DecodedResult = { decodedObjects: [], rest: buffer };
return decode(decodedResult);
}
export function decodeBuffer(data: Buffer): DecodedResult {
let result: DecodedResult = { decodedObjects: [], rest: data, isDone: false };

function decode(decodedResult: DecodedResult): DecodedResult {
if (decodedResult.rest.length === 0)
return decodedResult;

try {
const decodedObj = bencoder.decode(decodedResult.rest);
decodedResult.decodedObjects.push(decodedObj);
decodedResult.rest = Buffer.from('');
return decodedResult;
} catch (error) {
const errorMessage: string = error.message;
if (!!errorMessage && errorMessage.startsWith(CONTINUATION_ERROR_MESSAGE)) {
const unexpectedContinuation: string = errorMessage.slice(CONTINUATION_ERROR_MESSAGE.length, errorMessage.length - 1);

const rest = decodedResult.rest;
const encodedObj = rest.slice(0, rest.length - unexpectedContinuation.length);

decodedResult.decodedObjects.push(bencoder.decode(encodedObj));
decodedResult.rest = Buffer.from(unexpectedContinuation);

return decode(decodedResult);
} else {
return decodedResult;
while (result.rest.length > 0) {
const message = decodeNextMessage(result.rest);
if (!message.msg) {
break;
}

result.decodedObjects.push(message.msg);
result.rest = result.rest.slice(message.msgLen, result.rest.length);

if (message.msg.status && message.msg.status.indexOf('done') > -1) {
result.isDone = true;
break;
}
}

return result;
}
17 changes: 4 additions & 13 deletions src/nreplClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,16 +139,11 @@ const send = (msg: Message, connection?: CljConnectionInformation): Promise<any[
const respObjects: any[] = [];
client.on('data', data => {
nreplResp = Buffer.concat([nreplResp, data]);
const { decodedObjects, rest } = bencodeUtil.decodeObjects(nreplResp);
const { decodedObjects, rest, isDone } = bencodeUtil.decodeBuffer(nreplResp);
nreplResp = rest;
const validDecodedObjects = decodedObjects.reduce((objs, obj) => {
if (!isLastNreplObject(objs))
objs.push(obj);
return objs;
}, []);
respObjects.push(...validDecodedObjects);

if (isLastNreplObject(respObjects)) {
respObjects.push(...decodedObjects);

if (isDone) {
client.end();
client.removeAllListeners();
resolve(respObjects);
Expand All @@ -157,10 +152,6 @@ const send = (msg: Message, connection?: CljConnectionInformation): Promise<any[
});
};

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

export const nreplClient = {
complete,
Expand Down
135 changes: 135 additions & 0 deletions test/bencodeUtil.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import * as assert from 'assert';
import { decodeBuffer } from '../src/bencodeUtil';

interface DecodedResult {
decodedObjects: any[];
rest: Buffer;
isDone: boolean;
}

function decodeMessages(messages: string[]): DecodedResult {
let nreplResp = Buffer.from(''),
isDone = false;
const respObjects: any[] = [];

messages.forEach(item => {
nreplResp = Buffer.concat([nreplResp, Buffer.from(item)]);
const response = decodeBuffer(nreplResp);
nreplResp = response.rest;
isDone = response.isDone;
respObjects.push(...response.decodedObjects);
});

return { decodedObjects: respObjects, rest: nreplResp, isDone: isDone };
}

suite('bencodeUtil.decodeBuffer', function () {
test('create new session', () => {
const input = Buffer.from(
'd11:new-session36:58d1e5dc-c717-4864-bf49-e7750ced6f28'
+ '7:session36:7fcd096b-4ee4-4142-bb6b-6fc09e5c41606:statusl4:doneee'),
expected = {
'new-session': '58d1e5dc-c717-4864-bf49-e7750ced6f28',
'session': '7fcd096b-4ee4-4142-bb6b-6fc09e5c4160',
'status': ['done']
},
result = decodeBuffer(input);
assert.ok(result.isDone);
assert.deepEqual(result.decodedObjects, [expected]);
assert.equal(result.rest.length, 0);
});

test('close session', () => {
const input = Buffer.from(
'd7:session36:9968ec29-b87d-4e1f-8444-076280357dd36:statusl4:done14:session-closedee'),
expected = {
'session': '9968ec29-b87d-4e1f-8444-076280357dd3',
'status': ['done', 'session-closed']
},
result = decodeBuffer(input);
assert.ok(result.isDone);
assert.deepEqual(result.decodedObjects, [expected]);
assert.equal(result.rest.length, 0);
});

test('completion candidates', () => {
const input = Buffer.from(
'd11:completionsld9:candidate5:slurp2:ns12:clojure.core4:type8:functioned'
+ '9:candidate14:slingshot.test4:type9:namespaceed9:candidate'
+ '17:slingshot.support4:type9:namespaceed9:candidate19:slingshot.slingshot'
+ '4:type9:namespaceee7:session36:4d32206b-5161-40d2-a4e7-d1be6ec777756:statusl4:doneee'),
expected = {
'session': '4d32206b-5161-40d2-a4e7-d1be6ec77775',
'completions': [
{
'candidate': 'slurp',
'ns': 'clojure.core',
'type': 'function'
},
{
'candidate': 'slingshot.test',
'type': 'namespace'
},
{
'candidate': 'slingshot.support',
'type': 'namespace'
},
{
'candidate': 'slingshot.slingshot',
'type': 'namespace'
},
],
'status': ['done']
},
result = decodeBuffer(input);
assert.ok(result.isDone);
assert.deepEqual(result.decodedObjects, [expected]);
assert.equal(result.rest.length, 0);
});

test('eval simple printing expression', () => {
const messages = [
'd3:out7:"test"\n7:session36:9968ec29-b87d-4e1f-8444-076280357dd3e',
'd7:session36:9968ec29-b87d-4e1f-8444-076280357dd35:value3:niled'
+ '7:session36:9968ec29-b87d-4e1f-8444-076280357dd36:statusl4:doneee'
+ '18:changed-namespacesd13:cheshire.cored7:aliasesd7:factory16:cheshire.factory'
+ '3:gen17:cheshire.generate7:gen-seq21:cheshire.generate-seq5:parse14:cheshire.parsee'
+ '7:internsd11:*generator*de9:*opt-map*de13:copy-arglistsd8:arglists11:([dst'
],
expectedOut = {
'session': '9968ec29-b87d-4e1f-8444-076280357dd3',
'out': '"test"\n',
},
result = decodeMessages(messages);
assert.equal(result.decodedObjects.length, 3);
assert.deepEqual(result.decodedObjects[0], expectedOut);
assert.ok(result.isDone);
assert.notEqual(result.rest.length, 0);
});

test('eval expression with result divided by multiple messages', () => {
const messages = [
'd7:session36:9968ec29-b87d-4e1f-8444-076280357dd35:value184:'
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing e',
'lit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
' Ipsum dolor sit amet consectetur adipiscing elit ut aliquam.e'
+ 'd7:session36:9968ec29-b87d-4e1f-8444-076280357dd36:statusl4:doneee'
],
expectedWithValue = {
'session': '9968ec29-b87d-4e1f-8444-076280357dd3',
'value': 'Lorem ipsum dolor sit amet, consectetur adipiscing e'
+ 'lit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
+ ' Ipsum dolor sit amet consectetur adipiscing elit ut aliquam.'
},
expectedWithDone = {
'session': '9968ec29-b87d-4e1f-8444-076280357dd3',
'status': ['done']
},
result = decodeMessages(messages);
assert.equal(result.decodedObjects.length, 2);
assert.deepEqual(result.decodedObjects[0], expectedWithValue);
assert.deepEqual(result.decodedObjects[1], expectedWithDone);
assert.ok(result.isDone);
assert.equal(result.rest.length, 0);
});
});

0 comments on commit 94237b5

Please sign in to comment.