Skip to content

Commit

Permalink
Fixed #20
Browse files Browse the repository at this point in the history
  • Loading branch information
calebsander committed Aug 15, 2016
1 parent afba527 commit 3de5868
Show file tree
Hide file tree
Showing 15 changed files with 162 additions and 70 deletions.
16 changes: 11 additions & 5 deletions compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const uploadDownloadB = browserify();
uploadDownloadB.add(__dirname + '/client-side/upload-download.js');

let s = new Simultaneity;
//Replace require('util'), which is only used for util.inspect(), to minimize file size
for (let utilFile of ['/lib/assert', '/structure-types', '/read']) {
s.addTask(() => {
fs.createReadStream(__dirname + utilFile + '.js')
Expand All @@ -23,6 +24,8 @@ for (let utilFile of ['/lib/assert', '/structure-types', '/read']) {
});
}
s.addTask(() => {
//Load the upload and download code and append them to each other to make a combined include file
//These files are not too big, so it is not terrible to load them into memory
let uploadCode, downloadCode;
const loadS = new Simultaneity;
loadS.addTask(() => {
Expand Down Expand Up @@ -62,24 +65,27 @@ const downloadFiles = [
'/lib/util-inspect.js'
];
s.callback(() => {
//Include the file in the browserify result because it is require()d by other files
function exposeFile(b, name, fileName = name) {
b.require(__dirname + fileName, {expose: name});
}
function compile(b, {modifiedFiles, exposeFiles, outputFile}) {
console.log('Compiling: Browserifying and babelifying ' + outputFile);
//Expose the files with require('util') removed in place of the true file
for (let ending in modifiedFiles) {
for (let file of modifiedFiles[ending]) exposeFile(b, file + '.js', file + '-' + ending + '.js');
}
//Expose all the unmodified files as normal
for (let file of exposeFiles) exposeFile(b, file);
b.transform('babelify', {presets: ['es2015']});
b.transform('babelify', {presets: ['es2015']}); //babelify so it works in older browsers
const chunks = [];
b.bundle().on('data', chunk => chunks.push(chunk)).on('end', () => {
b.bundle().on('data', chunk => chunks.push(chunk)).on('end', () => { //load output into memory
console.log('Compiling: Uglifying ' + outputFile);
const uglified = uglify.minify(Buffer.concat(chunks).toString(), {
const uglified = uglify.minify(Buffer.concat(chunks).toString(), { //minify the code
fromString: true,
compress: {unsafe: true}
compress: {unsafe: true}
}).code;
fs.writeFile(__dirname + outputFile, uglified, err => {
fs.writeFile(__dirname + outputFile, uglified, err => { //write out the minified code
if (err) throw err;
});
});
Expand Down
6 changes: 2 additions & 4 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ assert.between(0, VERSION, 65536);
const VERSION_BYTES = 2;
const VERSION_BUFFER = new ArrayBuffer(VERSION_BYTES);
new DataView(VERSION_BUFFER).setUint16(0, VERSION);
const VERSION_STRING = base64.fromByteArray(new Uint8Array(VERSION_BUFFER));
const VERSION_STRING = base64.fromByteArray(new Uint8Array(VERSION_BUFFER)); //convert version number into string (used in type signatures)

module.exports = {
VERSION_STRING
};
module.exports = {VERSION_STRING};
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
*/

module.exports = {};
//Copy version string, io functions, and types into package exports
for (let sbModule of ['config', 'io', 'structure-types']) {
sbModule = require(__dirname + '/' + sbModule + '.js');
for (let attribute in sbModule) module.exports[attribute] = sbModule[attribute];
}
module.exports.r = require(__dirname + '/read.js');
module.exports.r = require(__dirname + '/read.js'); //add .r to read functions because type() and value() would be confusing
28 changes: 18 additions & 10 deletions io.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
//For use with browserify
if (__dirname === '/') __dirname = '';

//This file contains functions for performing I/O;
//specifically, reads and writes of types and values and HTTP responses

const assert = require(__dirname + '/lib/assert.js');
const BufferStream = require(__dirname + '/lib/buffer-stream.js');
const GrowableBuffer = require(__dirname + '/lib/growable-buffer.js');
Expand Down Expand Up @@ -123,7 +126,7 @@ const io = module.exports = {
let type;
try { type = r.type(toArrayBuffer(buffer), false) } //eslint-disable-line semi
catch (e) { callback(e, null) } //eslint-disable-line semi
if (type) callback(null, type);
if (type) callback(null, type); //if error occurred, don't callback with a null type
});
},
/**
Expand Down Expand Up @@ -159,7 +162,7 @@ const io = module.exports = {
let value;
try { value = r.value({buffer: toArrayBuffer(buffer), type}) } //eslint-disable-line semi
catch (e) { callback(e, null) } //eslint-disable-line semi
if (value) callback(null, value);
if (value) callback(null, value); //if error occurred, don't callback with a null value
});
},
/**
Expand Down Expand Up @@ -191,13 +194,14 @@ const io = module.exports = {
inStream.on('end', () => {
const buffer = Buffer.concat(segments);
let type;
//Using consumeType() in order to get the length of the type (the start of the value)
try { type = r._consumeType(toArrayBuffer(buffer), 0) } //eslint-disable-line semi
catch (e) { callback(e, null, null) } //eslint-disable-line semi
if (type) {
let value;
try { value = r.value({buffer: toArrayBuffer(buffer), offset: type.length, type: type.value}) } //eslint-disable-line semi
catch (e) { callback(e, null, null) } //eslint-disable-line semi
if (value) callback(null, type.value, value);
if (value) callback(null, type.value, value); //if error occurred, don't callback with null value or type
}
});
},
Expand All @@ -224,13 +228,17 @@ const io = module.exports = {
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Encoding', 'gzip');
res.setHeader('sig', type.getSignature());
const outStream = zlib.createGzip(); //eslint-disable-line no-undef
if (req.headers.sig && req.headers.sig === type.getSignature()) io.writeValue({type, value, outStream}, err => {
if (err) callback(err);
});
else io.writeTypeAndValue({type, value, outStream}, err => {
if (err) callback(err);
});
const outStream = zlib.createGzip(); //pipe into a zip stream to decrease size of response
if (req.headers.sig && req.headers.sig === type.getSignature()) { //if client already has type, only value needs to be sent
io.writeValue({type, value, outStream}, err => {
if (err) callback(err);
});
}
else { //otherwise, type and value need to be sent
io.writeTypeAndValue({type, value, outStream}, err => {
if (err) callback(err);
});
}
outStream.pipe(res).on('finish', () => callback(null));
}
catch (err) { callback(err) }
Expand Down
25 changes: 21 additions & 4 deletions lib/assert.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,55 @@
const util = require('util');

//A number of useful assertion functions
//Used in tests and for validations in production
const assert = {
//Assert that the instance is an instance of the constructor, or at least one of the constructors, or a subclass
instanceOf(instance, constructors) {
if (!(constructors instanceof Array)) constructors = [constructors];
let constructorMatched = false;
for (let constructor of constructors) {
if (instance instanceof constructor || (instance !== undefined && instance !== null && instance.constructor === constructor)) { //second case is necessary for primitives
if (
instance instanceof constructor ||
(!(instance === undefined || instance === null) && instance.constructor === constructor) //necessary for primitives
) {
constructorMatched = true;
break;
}
}
if (!constructorMatched) throw new TypeError(util.inspect(instance) + ' is not an instance of ' + constructors.map((constructor) => constructor.name).join(' or '));
},
//Assert that a number is an integer (within the +/-2^53 that can be represented precisely in a double)
integer(instance) {
assert.instanceOf(instance, Number);
if (!Number.isSafeInteger(instance)) throw new RangeError(util.inspect(instance) + ' is not an integer');
},
//Assert that a number is between the specified values, with an optional message
between(lower, value, upper, message) {
if (value < lower || value >= upper) {
const errorMessage = util.inspect(value) + ' is not in [' + util.inspect(lower) + ',' + util.inspect(upper) + ')';
if (message === undefined) throw new RangeError(errorMessage);
else throw new RangeError(message + ' (' + errorMessage + ')');
}
},
//Assert that a number fits in an unsigned byte
byteUnsignedInteger(value) {
assert.integer(value);
assert.between(0, value, 256);
},
//Assert that a number fits in an unsigned integer
fourByteUnsignedInteger(value) {
assert.integer(value);
assert.between(0, value, 4294967296);
},
//Throw an error
fail(message) {
throw new Error(message);
},
//Assert that a condition is met; if not, throw an error with the specified message
assert(condition, message) { //eslint-disable-line no-unreachable
if (!condition) assert.fail(message);
},
//Assert that the execution of a function throws an error, and that the error message matches the specified one
throws(block, message) {
let success = true;
try {
Expand All @@ -48,6 +61,8 @@ const assert = {
}
assert.assert(success, 'Should throw an error');
},
//Assert that two values are "equal"
//What this means depends a lot on the type of the expected value
equal(actual, expected) {
const error = new RangeError('Expected ' + util.inspect(expected) + ' but got ' + util.inspect(actual));
if (expected && expected.constructor === Object) {
Expand Down Expand Up @@ -102,19 +117,21 @@ const assert = {
}
catch (e) { throw error }
}
else if (!(expected === undefined || expected === null) && expected.equals instanceof Function) {
else if (!(expected === undefined || expected === null) && expected.equals instanceof Function) { //if expected has an equals function, use it
let equals;
try { equals = expected.equals(actual) } //eslint-disable-line semi
catch (e) { throw new Error('equals() is not implemented for ' + util.inspect(expected)) } //eslint-disable-line semi
if (!equals) throw error;
}
else {
else { //use primitive equality if nothing else matches
if (expected !== actual) throw error;
}
},
//Assert that an error's message begins with the specified text
message(err, message) {
assert.instanceOf(message, String);
assert.assert(
message && err && err.message.startsWith(message),
err && err.message.startsWith(message),
'Message "' + (err ? err.message : 'No error thrown') + '" does not start with "' + message + '"'
);
}
Expand Down
4 changes: 2 additions & 2 deletions lib/bit-math.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module.exports = {
dividedByEight: (n) => n >>> 3,
modEight: (n) => n & 0b111
dividedByEight: n => n >>> 3, //efficiently divide by 8
modEight: n => n & 0b111 //efficiently mod 8
};
4 changes: 2 additions & 2 deletions lib/buffer-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ class BufferStream extends stream.Readable {
}
else if (buffer && buffer.constructor === GrowableBuffer) {
this.buffer = buffer.rawBuffer;
this.end = buffer.length;
this.end = buffer.length; //end earlier than the end of the raw buffer
}
else assert.fail('Expected ArrayBuffer or GrowableBuffer, got ' + (buffer ? buffer.constructor.name : String(buffer))); //buffer should always have a constructor if it is neither undefined nor null
this.offset = 0;
}
_read(size) {
if (this.offset < this.end) {
this.push(Buffer.from(this.buffer.slice(this.offset, Math.min(this.offset + size, this.end))));
this.push(Buffer.from(this.buffer.slice(this.offset, Math.min(this.offset + size, this.end)))); //slice behaves nicely when going past the end of the buffer
this.offset += size;
}
else {
Expand Down
6 changes: 6 additions & 0 deletions lib/buffer-string.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const assert = require(__dirname + '/assert.js');
const GrowableBuffer = require(__dirname + '/growable-buffer.js');

//Arbitrarily set; fairly low to be safe
const MAX_ARGUMENTS_LENGTH = 0x1000;

module.exports = {
//Convert bytes to a UTF-8 string
toString(buffer) {
if (buffer && buffer.constructor === ArrayBuffer) buffer = new Uint8Array(buffer);
assert.instanceOf(buffer, Uint8Array);
Expand Down Expand Up @@ -70,6 +72,7 @@ module.exports = {
}
return string;
},
//Convert a string to UTF-8 bytes;
fromString(string) {
assert.instanceOf(string, String);
//Taken from http://stackoverflow.com/a/18729931
Expand All @@ -96,13 +99,16 @@ module.exports = {
}
return utf8.toBuffer();
},
//Convert bytes to a string where each character represents one byte
//Used for representing ArrayBuffers as keys in maps
toBinaryString(buffer) {
assert.instanceOf(buffer, ArrayBuffer);
let string = '';
const castBuffer = new Uint8Array(buffer);
for (let byte of castBuffer) string += String.fromCharCode(byte);
return string;
},
//Convert a string generated by toBinaryString() back into bytes
fromBinaryString(string) {
assert.instanceOf(string, String);
const buffer = new ArrayBuffer(string.length);
Expand Down
2 changes: 1 addition & 1 deletion lib/growable-buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class GrowableBuffer {
*/
grow(size) {
assert.integer(size);
if (size > this.buffer.byteLength) {
if (size > this.buffer.byteLength) { //if resizing is necessary
const newBuffer = new ArrayBuffer(size << 1);
new Uint8Array(newBuffer).set(new Uint8Array(this.buffer).subarray(0, this.size));
this.buffer = newBuffer;
Expand Down
12 changes: 8 additions & 4 deletions lib/replace-stream.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const assert = require(__dirname + '/assert.js');
const stream = require('stream');

//Transforms an input into an output by doing simple replacements
//Assumes input is in ASCII-7 (so that code points don't get cut by chunk boundaries)
module.exports = class extends stream.Transform {
constructor(match, result) {
super();
Expand All @@ -15,14 +17,15 @@ module.exports = class extends stream.Transform {
for (let i = 0; i < chunk.length; i++) {
let char = chunk[i];
if (char === this.match[this.matching]) {
this.matching++;
if (this.matching === this.match.length) {
this.matching++; //keep track of the number of characters matched
if (this.matching === this.match.length) { //if whole string is matched, push out the result
this.push(this.result);
this.matching = 0;
this.matching = 0; //reset number of characters matched
}
}
else {
else { //character doesn't match, so push all stored characters
this.push(chunk.substring(i - this.matching, i));
//Reset match index to start of string
if (char === this.match[0]) this.matching = 1;
else {
this.push(char);
Expand All @@ -34,6 +37,7 @@ module.exports = class extends stream.Transform {
if (callback !== undefined) callback();
}
_flush(callback) {
//Deal with any residual matching characters
if (this.matching) this.push(this.lastChunk.substring(this.lastChunk.length - this.matching));
if (callback !== undefined) callback();
}
Expand Down
18 changes: 17 additions & 1 deletion lib/simultaneity.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,36 @@
const openSimultaneities = new Set;

/*
Runs multiple asynchronous tasks with a callback for when they have all finished
Usage:
let s = new Simultaneity;
s.addTask(() => {
asynchSomething(() => {
...;
s.taskFinished();
});
});
...;
s.callback(() => {...});
*/
module.exports = class {
constructor() {
this.tasks = new Set;
this.finishedCount = 0;
}
//Adds a routine to be executed to start an asynchronous task
addTask(start) {
this.tasks.add(start);
}
//Should be called whenever an asynchronous task completes
taskFinished() {
this.finishedCount++;
if (this.finishedCount === this.tasks.size) {
if (this.finishedCount === this.tasks.size) { //all tasks have ended
openSimultaneities.delete(this);
this.done();
}
}
//Set a callback for when all the tasks finish and start all the tasks
callback(callback) {
this.done = callback;
openSimultaneities.add(this);
Expand Down
1 change: 1 addition & 0 deletions lib/strint.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/*eslint-disable*/
//From https://github.com/calebsander/strint, mostly not my code
var e = {};

/*istanbul ignore next*/
Expand Down
2 changes: 2 additions & 0 deletions lib/util-inspect.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//A replacement for util.inspect
//Not quite as complex and doesn't handle all the cases, but sufficient
exports.inspect = obj => {
if (obj === undefined) return 'undefined';
if (obj === null || (
Expand Down
Loading

0 comments on commit 3de5868

Please sign in to comment.