diff --git a/exec/load/dns.js b/exec/load/dns.js
new file mode 100644
index 0000000000000000000000000000000000000000..bbe4a374389eb5dd60ef1baa7d489c7e6a599d62
--- /dev/null
+++ b/exec/load/dns.js
@@ -0,0 +1,634 @@
+/* DNS protocol library
+ * Uses events, so the calling script *MUST* set js.do_callbacks
+ * Example usage:
+ *
+ * js.do_callbacks = true;
+ * load('dns.js');
+ *
+ * function handle(resp)
+ * {
+ * 	log(LOG_ERROR, JSON.stringify(resp));
+ * 	exit(0);
+ * }
+ *
+ * dns.resolve('example.com', handle);
+ *
+ */
+
+require('sockdefs.js', 'SOCK_DGRAM');
+
+function DNS(servers) {
+	if (servers === undefined)
+		servers = system.name_servers;
+	var nextid = 0;
+	var outstanding = {};
+	this.sockets = [];
+
+	handle_response = function() {
+		var resp;
+		var id;
+		var offset = 0;
+		var queries;
+		var answers;
+		var nameservers;
+		var arecords;
+		var q;
+		var i;
+		var ret = {'queries':[], 'answers':[], 'nameservers':[], 'additional':[]};
+		var rdata;
+		var rdlen;
+		var tmp;
+
+		string_to_int16 = function(str) {
+			return ((ascii(str[0])<<8) | (ascii(str[1])));
+		}
+
+		string_to_int32 = function(str) {
+			return ((ascii(str[0])<<24) | (ascii(str[1]) << 16) | (ascii(str[1]) << 8) | (ascii(str[1])));
+		}
+
+		function get_string(resp, offset) {
+			var len = ascii(resp[offset]);
+			return {'len':len + 1, 'string':resp.substr(offset + 1, len)};
+		}
+
+		function get_name(resp, offset) {
+			var len;
+			var name = '';
+			var ret = 0;
+			var compressed = false;
+
+			do {
+				len = ascii(resp[offset]);
+				if (!compressed)
+					ret++;
+				offset++;
+				if (len > 63) {
+					offset = ((len & 0x3f) << 8) | ascii(resp[offset]);
+					if (!compressed)
+						ret++;
+					compressed = true;
+				}
+				else {
+					if (!compressed)
+						ret += len;
+					if (name.length > 0 && len > 0)
+						name += '.';
+					name += resp.substr(offset, len);
+					offset += len;
+				}
+			} while (len != 0);
+
+			return {'len':ret, 'name':name};
+		}
+
+		function parse_rdata(type, resp, offset, len) {
+			var tmp;
+			var tmp2;
+			var tmp3;
+			var tmp4;
+
+			switch(type) {
+				case 1:	// A
+					return ascii(resp[offset]) + '.' +
+					       ascii(resp[offset + 1]) + '.' +
+					       ascii(resp[offset + 2]) + '.' +
+					       ascii(resp[offset + 3]);
+				case 2:  // NS
+					return get_name(resp, offset).name;
+				case 5:  // CNAME
+					return get_name(resp, offset).name;
+				case 6:  // SOA
+					tmp = {};
+					tmp2 = 0;
+					tmp3 = get_name(resp, offset + tmp2);
+					tmp.mname = tmp3.name;
+					tmp2 += tmp3.len;
+					tmp3 = get_name(resp, offset + tmp2);
+					tmp.rname = tmp3.name;
+					tmp2 += tmp3.len;
+					tmp.serial = string_to_int32(resp.substr(offset + tmp2, 4));
+					tmp2 += 4;
+					tmp.refresh = string_to_int32(resp.substr(offset + tmp2, 4));
+					tmp2 += 4;
+					tmp.retry = string_to_int32(resp.substr(offset + tmp2, 4));
+					tmp2 += 4;
+					tmp.export = string_to_int32(resp.substr(offset + tmp2, 4));
+					tmp2 += 4;
+					tmp.minimum = string_to_int32(resp.substr(offset + tmp2, 4));
+					tmp2 += 4;
+					return tmp;
+				case 11: // WKS
+					tmp = {};
+					tmp.address = ascii(resp[offset]) + '.' +
+					              ascii(resp[offset + 1]) + '.' +
+					              ascii(resp[offset + 2]) + '.' +
+					              ascii(resp[offset + 3]);
+					tmp.protocol = ascii(resp[offset + 4]);
+					tmp2 = 5;
+					tmp.ports = [];
+					while (tmp2 < len) {
+						tmp3 = ascii(resp[offset + tmp2]);
+						for (tmp4 = 0; tmp4 < 8; tmp4++) {
+							if (tmp3 & (1 << tmp4))
+								tmp.ports.push(8 * (tmp2 - 5) + tmp3);
+						}
+					}
+					return tmp;
+				case 12: // PTR
+					return get_name(resp, offset).name;
+				case 13: // HINFO
+					tmp = get_string(resp, offset);
+					return {'cpu':tmp.string, 'os':get_string(resp, offset + tmp.len).string};
+				case 15: // MX
+					tmp = {};
+					tmp.preference = string_to_int16(resp.substr(offset, 2));
+					tmp.exchange = get_name(resp, offset + 2).name;
+					return tmp;
+				case 16: // TXT
+					tmp = [];
+					tmp2 = 0;
+					do {
+						tmp3 = get_string(resp, offset + tmp2);
+						tmp.push(tmp3.string);
+						tmp2 += tmp3.len;
+					} while (tmp2 < len);
+					return tmp;
+				case 28:
+					return format("%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x",
+					               ascii(resp[offset + 0]),  ascii(resp[offset + 1]),
+					               ascii(resp[offset + 2]),  ascii(resp[offset + 3]),
+					               ascii(resp[offset + 4]),  ascii(resp[offset + 5]),
+					               ascii(resp[offset + 6]),  ascii(resp[offset + 7]), 
+					               ascii(resp[offset + 8]),  ascii(resp[offset + 9]),
+					               ascii(resp[offset + 10]), ascii(resp[offset + 11]),
+					               ascii(resp[offset + 12]), ascii(resp[offset + 13]),
+					               ascii(resp[offset + 14]), ascii(resp[offset + 15])
+					             ).replace(/(0000:)+/, ':').replace(/(^|:)0{1,3}/g, '$1');
+				case 3:  // MD
+				case 4:  // MF
+				case 7:  // MB
+				case 8:  // MG
+				case 9:  // MR
+				case 10: // NULL
+				case 14: // MINFO
+					return {'raw':resp.substr(offset, len)};
+			}
+		}
+
+		resp = this.recv(10000);
+		id = string_to_int16(resp);
+		if (this.outstanding[id] === undefined)
+			return false;
+
+		q = this.outstanding[id];
+		delete this.outstanding[id];
+
+		ret.id = id;
+		ret.response = !!(ascii(resp[2]) & 1);
+		ret.opcode = (ascii(resp[2]) & 0x1e) >> 1;
+		ret.authoritative = !!(ascii(resp[2]) & (1<<5));
+		ret.truncation = !!(ascii(resp[2]) & (1<<6));
+		ret.recusrion = !!(ascii(resp[2]) & (1<<7));
+		ret.reserved = ascii(resp[3]) & 7;
+		ret.rcode = ascii(resp[3] & 0xf1) >> 3;
+
+		queries = string_to_int16(resp.substr(4, 2));
+		answers = string_to_int16(resp.substr(6, 2));
+		nameservers = string_to_int16(resp.substr(8, 2));
+		arecords = string_to_int16(resp.substr(10, 2));
+		if (answers === 0)
+			return false;
+		js.clearTimeout(q.timeoutid);
+		offset = 12;
+		for (i = 0; i < queries; i++) {
+			rdata = {};
+			tmp = get_name(resp, offset);
+			rdata.name = tmp.name;
+			offset += tmp.len;
+			rdata.type = string_to_int16(resp.substr(offset, 2));
+			offset += 2;
+			rdata.class = string_to_int16(resp.substr(offset, 2));
+			offset += 2;
+			ret.queries.push(rdata);
+		}
+
+		for (i = 0; i < answers; i++) {
+			rdata = {};
+			tmp = get_name(resp, offset);
+			rdata.name = tmp.name;
+			offset += tmp.len;
+			rdata.type = string_to_int16(resp.substr(offset, 2));
+			offset += 2;
+			rdata.class = string_to_int16(resp.substr(offset, 2));
+			offset += 2;
+			rdata.ttl = string_to_int32(resp.substr(offset, 4));
+			offset += 4;
+			rdlen = string_to_int16(resp.substr(offset, 2));
+			offset += 2;
+			rdata.rdata = parse_rdata(rdata.type, resp, offset, rdlen);
+			offset += rdlen;
+			ret.answers.push(rdata);
+		}
+
+		for (i = 0; i < nameservers; i++) {
+			rdata = {};
+			tmp = get_name(resp, offset);
+			rdata.name = tmp.name;
+			offset += tmp.len;
+			rdata.type = string_to_int16(resp.substr(offset, 2));
+			offset += 2;
+			rdata.class = string_to_int16(resp.substr(offset, 2));
+			offset += 2;
+			rdata.ttl = string_to_int32(resp.substr(offset, 4));
+			offset += 4;
+			rdlen = string_to_int16(resp.substr(offset, 2));
+			offset += 2;
+			rdata.rdata = parse_rdata(rdata.type, resp, offset, rdlen);
+			offset += rdlen;
+			ret.nameservers.push(rdata);
+		}
+
+		for (i = 0; i < arecords; i++) {
+			rdata = {};
+			tmp = get_name(resp, offset);
+			rdata.name = tmp.name;
+			offset += tmp.len;
+			rdata.type = string_to_int16(resp.substr(offset, 2));
+			offset += 2;
+			rdata.class = string_to_int16(resp.substr(offset, 2));
+			offset += 2;
+			rdata.ttl = string_to_int32(resp.substr(offset, 4));
+			offset += 4;
+			rdlen = string_to_int16(resp.substr(offset, 2));
+			offset += 2;
+			rdata.rdata = parse_rdata(rdata.type, resp, offset, rdlen);
+			offset += rdlen;
+			ret.additional.push(rdata);
+		}
+
+		q.callback.call(q.thisObj, ret);
+		return true;
+	}
+
+	servers.forEach(function(server) {
+		var sock = new Socket(SOCK_DGRAM, "dns", server.indexOf(':') >= 0);
+		sock.bind();
+		sock.connect(server, 53);
+		sock.on('read', handle_response);
+		sock.outstanding = outstanding;
+		this.sockets.push(sock);
+	}, this);
+
+	if (this.sockets.length < 1)
+		throw('Unable to create any sockets');
+
+	increment_id = function() {
+		var ret = nextid;
+		do {
+			nextid++;
+			if (nextid > 65535)
+				nextid = 0;
+		} while (outstanding[nextid] !== undefined);
+		return ret;
+	}
+
+}
+
+DNS.types = {
+	'A':1,
+	'NS':2,
+	'MD':3,
+	'MF':4,
+	'CNAME':5,
+	'SOA':6,
+	'MB':7,
+	'MG':8,
+	'MR':9,
+	'NULL':10,
+	'WKS':11,
+	'PTR':12,
+	'HINFO':13,
+	'MINFO':14,
+	'MX':15,
+	'TXT':16,
+	'AAAA':28
+};
+
+DNS.classes = {
+	'IN':1,
+	'CS':2,
+	'CH':3,
+	'HS':4
+};
+
+DNS.prototype.query = function(queries, /* queryStr, type, class, */callback, thisObj, recursive, timeout, failures, failed) {
+	var id;
+	var namebits = {};
+
+	function int16_to_string(id) {
+		return ascii((id & 0xff00) >> 8) + ascii(id & 0xff);
+	}
+
+	function handle_timeout() {
+		this.failed++;
+
+		delete outstanding[this.id];
+
+		if (this.failed > 2)
+			this.callback.call(this);
+		else
+			this.obj.query(this.query, this.callback, this.thisObj, this.recursive, this.timeout, this.failures, this.failed);
+	}
+
+	queries.forEach(function(query) {
+		if (query.str === undefined)
+			query.str = 'example.com';
+		if (query.type === undefined)
+			query.type = 'AAAA';
+		if (DNS.types[query.type] !== undefined)
+			query.type = DNS.types[query.type];
+		query.type = parseInt(query.type, 10);
+		if (isNaN(query.type)) {
+			throw new Error('Invalid type');
+		}
+		if (query.class === undefined)
+			query.class = 'IN';
+		if (DNS.classes[query.class] !== undefined)
+			query.class = DNS.classes[query.class];
+		query.class = parseInt(query.class, 10);
+		if (isNaN(query.class)) {
+			throw new Error('Invalid class');
+		}
+	});
+
+	if (callback === undefined)
+		return;
+	if (recursive === undefined)
+		recursive = true;
+	if (timeout === undefined)
+		timeout = 1000;
+	if (failures === undefined)
+		failures = 1;
+	if (failed === undefined)
+		failed = 0;
+
+	var query = '';
+
+	// Header
+	id = increment_id();
+	query = int16_to_string(id);
+	query += ascii(recursive ? 1 : 0);
+	query += ascii(0);
+	query += int16_to_string(queries.length);	// Questions
+	query += int16_to_string(0);	// Answers
+	query += int16_to_string(0);	// Name Servers
+	query += int16_to_string(0);	// Additional Records
+
+	// Queries
+	queries.forEach(function(oneQuery) {
+		var thisname = '';
+		var fields = oneQuery.query.split(/\./).reverse();
+		var matched;
+
+		fields.forEach(function(field) {
+			if (field.length > 63)
+				throw new Error('invalid field in query "'+field+'" (longer than 63 characters)');
+			thisname = ascii(field.length) + field + thisname;
+			if (namebits[thisname] !== undefined && namebits[thisname] > 0) {
+				matched = {'offset':namebits[thisname], 'length':thisname.length};
+			}
+			else {
+				namebits[thisname] = 0 - thisname.length;
+			}
+		});
+
+		// Now fix up negative namebits...
+		Object.keys(namebits).forEach(function(name) {
+			if (namebits[name] < 0) {
+				namebits[name] = query.length + thisname.length + namebits[name];
+			}
+		});
+
+		// And use the match
+		if (matched !== undefined) {
+			thisname = thisname.substr(0, thisname.length - matched.length);
+			thisname = thisname + ascii(0xc0 | ((matched.offset & 0x3f) >> 8)) + ascii(matched.offset & 0xff);
+		}
+		else
+			thisname += ascii(0);
+		query += thisname;
+		query += int16_to_string(oneQuery.type);
+		query += int16_to_string(oneQuery.class);
+	}, this);
+
+	this.sockets[0].outstanding[id] = {'callback':callback, 'query':queries, 'recursive':recursive, 'timeout':timeout, 'thisObj':thisObj, 'id':id, 'obj':this, 'failed':failed};
+	this.sockets[0].outstanding[id].timeoutid = js.setTimeout(handle_timeout, timeout, this.sockets[0].outstanding[id]);
+
+	this.sockets.forEach(function(sock) {
+		sock.write(query);
+	});
+}
+
+DNS.prototype.resolve = function(host, callback, thisObj)
+{
+	var ctx = {A:{}, AAAA:{}, unique_id:'DNS.prototype.resolve'};
+	var final;
+	var respA;
+	var respAAAA;
+
+	this.sockets.forEach(function(sock) {
+		ctx.unique_id += '.'+sock.local_port;
+	});
+
+	if (host === undefined)
+		throw new Error('No host specified');
+
+	if (thisObj === undefined)
+		thisObj = this;
+
+	ctx.callback = callback;
+	ctx.thisObj = thisObj;
+
+	ctx.final = js.addEventListener(ctx.unique_id+'.final', function() {
+		var ret = [];
+		this.AAAA.addrs.forEach(function(addr) {
+			ret.push(addr);
+		});
+		this.A.addrs.forEach(function(addr) {
+			ret.push(addr);
+		});
+		js.removeEventListener(this.final);
+		js.removeEventListener(this.respA);
+		js.removeEventListener(this.respAAAA);
+		this.callback.call(this.thisObj, ret);
+	});
+
+	ctx.respA = js.addEventListener(ctx.unique_id+'.respA', function() {
+		this.A.done = true;
+		if (this.AAAA.done)
+			js.dispatchEvent(this.unique_id + '.final', this);
+	});
+
+	ctx.respAAAA = js.addEventListener(ctx.unique_id+'.respAAAA', function() {
+		this.AAAA.done = true;
+		if (this.A.done)
+			js.dispatchEvent(ctx.unique_id + '.final', this);
+	});
+
+	function handle_response(resp) {
+		var rectype;
+		switch(resp.queries[0].type) {
+			case DNS.types.A:
+				rectype = 'A';
+				break;
+			case DNS.types.AAAA:
+				rectype = 'AAAA';
+				break;
+		};
+		if (rectype === undefined)
+			return;
+
+		this[rectype].addrs = [];
+
+		resp.answers.forEach(function(ans) {
+			if (resp.queries[0].type != ans.type || resp.queries[0].class != ans.class)
+				return;
+			this[rectype].addrs.push(ans.rdata);
+		}, this);
+		js.dispatchEvent(this.unique_id + '.resp'+rectype, this);
+	}
+
+	this.query([{query:host, type:'AAAA'}], handle_response, ctx);
+	this.query([{query:host, type:'A'}], handle_response, ctx);
+}
+
+DNS.prototype.resolveTypeClass = function(host, type, class, callback, thisObj)
+{
+	var ctx = {};
+	var final;
+	var respA;
+	var respAAAA;
+
+	this.sockets.forEach(function(sock) {
+		ctx.unique_id += '.'+sock.local_port;
+	});
+
+	if (host === undefined)
+		throw new Error('No host specified');
+
+	if (type === undefined)
+		throw new Error('No type specified');
+
+	if (class === undefined)
+		throw new Error('No class specified');
+
+	if (callback === undefined)
+		throw new Error('No callback specified');
+	ctx.callback = callback;
+
+	if (thisObj === undefined)
+		thisObj = this;
+	ctx.thisObj = thisObj;
+
+	function handle_response(resp) {
+		ret = [];
+		if (resp !== undefined && resp.answers !== undefined) {
+			resp.answers.forEach(function(ans) {
+				if (resp.queries[0].type != ans.type || resp.queries[0].class != ans.class)
+					return;
+				ret.push(ans.rdata);
+			});
+		}
+		this.callback.call(this.thisObj, ret);
+	}
+
+	this.query([{query:host, type:type, class:class}], handle_response, ctx);
+}
+
+DNS.prototype.reverse = function(ip, callback, thisObj)
+{
+	var qstr;
+	var m;
+	var a;
+	var fillpos;
+
+	if (ip === undefined)
+		throw new Error('No IP specified');
+
+	if (thisObj === undefined)
+		thisObj = this;
+
+	// Sure, this doesn't deal with terrible ipv4-mapped representations.  Suck it.
+	m = ip.match(/^(?:::ffff:)?([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i);
+	if (m !== null) {
+		// IPv4 Address
+		if (parseInt(m[1], 10) > 255 || parseInt(m[2], 10) > 255 || parseInt(m[3], 10) > 255 || parseInt(m[4], 10) > 255)
+			throw new Error('Malformed IP address '+ip);
+		qstr = m[4] + '.' + m[3] + '.' + m[2] + '.' + m[1] + '.in-addr.arpa';
+	}
+	else {
+		a = ip.split(/:/);
+		if (a.length < 3 || a.length > 8)
+			throw new Error('Malformed IP address '+ip);
+		if (ip.search(/^[a-fA-F0-9:]+$/) != 0)
+			throw new Error('Malformed IP address '+ip);
+		a.forEach(function(piece, idx, arr) {
+			if (piece !== '') {
+				while (piece.length < 4)
+					piece = '0'+piece;
+			}
+			arr[idx] = piece;
+		});
+		if (a[0] == '')
+			a[0] = '0000';
+		if (a[a.length - 1] == '')
+			a[a.length] = '0000';
+		while (a.length < 8) {
+			fillpos = a.indexOf('');
+			if (fillpos === -1)
+				throw('Unable to expand IPv6 address');
+			a.splice(fillpos, 0, '0000');
+		}
+		fillpos = a.indexOf('');
+		if (fillpos != -1)
+			a.splice(fillpos, 1, '0000');
+		a.reverse();
+		qstr = '';
+		a.forEach(function(piece) {
+			qstr += piece[3] + '.' + piece[2] + '.' + piece[1] + '.' + piece[0] + '.';
+		});
+		qstr += 'ip6.arpa';
+	}
+
+	this.resolveTypeClass(qstr, 'PTR', 'IN', callback, thisObj);
+}
+
+DNS.prototype.resolveMX = function(host, callback, thisObj)
+{
+	var ctx = {callback:callback};
+	var qstr;
+	var m;
+	var a;
+	var fillpos;
+
+	if (host === undefined)
+		throw new Error('No host specified');
+
+	if (thisObj === undefined)
+		thisObj = this;
+	ctx.thisObj = thisObj;
+
+	function handler(resp) {
+		var ret = [];
+		resp.sort(function(a, b) {
+			return a.preference - b.preference;
+		});
+		resp.forEach(function(r) {
+			ret.push(r.exchange);
+		});
+		this.callback.call(this.thisObj, ret);
+	}
+
+	this.resolveTypeClass(host, 'MX', 'IN', handler, ctx);
+}
diff --git a/src/sbbs3/exec.cpp b/src/sbbs3/exec.cpp
index f89056069053b115f3d144eb7196a4378e8abdc7..7900c368f3ce61892d06f1040bba01c5184a6dcc 100644
--- a/src/sbbs3/exec.cpp
+++ b/src/sbbs3/exec.cpp
@@ -683,6 +683,7 @@ long sbbs_t::js_execfile(const char *cmd, const char* startup_dir, JSObject* sco
 	}
 	js_PrepareToExecute(js_cx, js_glob, path, startup_dir, js_scope);
 	JS_ExecuteScript(js_cx, js_scope, js_script, &rval);
+	js_handle_events(js_cx, &js_callback, &terminated);
 	sys_status &=~ SS_ABORT;
 
 	JS_GetProperty(js_cx, js_scope, "exit_code", &rval);
diff --git a/src/sbbs3/js_console.cpp b/src/sbbs3/js_console.cpp
index 0f05db23e494066170cd448aaa5359e8b8fd4f0a..02c0385502f61b5cd770a48cb3f88f552e5806da 100644
--- a/src/sbbs3/js_console.cpp
+++ b/src/sbbs3/js_console.cpp
@@ -1998,6 +1998,21 @@ js_getlines(JSContext *cx, uintN argc, jsval *arglist)
     return(JS_TRUE);
 }
 
+void
+js_do_lock_input(JSContext *cx, JSBool lock)
+{
+	sbbs_t*		sbbs;
+
+	if ((sbbs = (sbbs_t*)JS_GetContextPrivate(cx)) == NULL)
+		return;
+
+	if(lock) {
+		pthread_mutex_lock(&sbbs->input_thread_mutex);
+	} else {
+		pthread_mutex_unlock(&sbbs->input_thread_mutex);
+	}
+}
+
 static JSBool
 js_lock_input(JSContext *cx, uintN argc, jsval *arglist)
 {
@@ -2109,6 +2124,136 @@ js_term_updated(JSContext *cx, uintN argc, jsval *arglist)
     return JS_TRUE;
 }
 
+size_t
+js_cx_input_pending(JSContext *cx)
+{
+	sbbs_t*		sbbs;
+
+	if ((sbbs = (sbbs_t*)JS_GetContextPrivate(cx)) == NULL)
+		return 0;
+
+	return sbbs->keybuf_level() + RingBufFull(&sbbs->inbuf);
+}
+
+static JSBool
+js_install_event(JSContext *cx, uintN argc, jsval *arglist, BOOL once)
+{
+	jsval	*argv=JS_ARGV(cx, arglist);
+	js_callback_t*	cb;
+	JSObject *obj=JS_THIS_OBJECT(cx, arglist);
+	JSFunction *ecb;
+	char operation[16];
+	enum js_event_type et;
+	size_t slen;
+	struct js_event_list *ev;
+	sbbs_t *sbbs;
+
+	if((sbbs=(sbbs_t*)js_GetClassPrivate(cx, JS_THIS_OBJECT(cx, arglist), &js_console_class))==NULL)
+		return(JS_FALSE);
+
+	if (argc != 2) {
+		JS_ReportError(cx, "console.on() and console.once() require exactly two parameters");
+		return JS_FALSE;
+	}
+	ecb = JS_ValueToFunction(cx, argv[1]);
+	if (ecb == NULL) {
+		return JS_FALSE;
+	}
+	JSVALUE_TO_STRBUF(cx, argv[0], operation, sizeof(operation), &slen);
+	HANDLE_PENDING(cx, NULL);
+	if (strcmp(operation, "read") == 0) {
+		if (once)
+			et = JS_EVENT_CONSOLE_INPUT_ONCE;
+		else
+			et = JS_EVENT_CONSOLE_INPUT;
+	}
+	else {
+		JS_ReportError(cx, "event parameter must be 'read'");
+		return JS_FALSE;
+	}
+
+	cb = &sbbs->js_callback;
+	if (cb == NULL) {
+		return JS_FALSE;
+	}
+	if (!cb->events_supported) {
+		JS_ReportError(cx, "events not supported");
+		return JS_FALSE;
+	}
+
+	ev = (struct js_event_list *)malloc(sizeof(*ev));
+	if (ev == NULL) {
+		JS_ReportError(cx, "error allocating %lu bytes", sizeof(*ev));
+		return JS_FALSE;
+	}
+	ev->prev = NULL;
+	ev->next = cb->events;
+	if (ev->next)
+		ev->next->prev = ev;
+	ev->type = et;
+	ev->cx = obj;
+	JS_AddObjectRoot(cx, &ev->cx);
+	ev->cb = ecb;
+	ev->id = cb->next_eid++;
+	ev->data.sock = sbbs->client_socket;
+	cb->events = ev;
+
+	JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(ev->id));
+	return JS_TRUE;
+}
+
+static JSBool
+js_clear_console_event(JSContext *cx, uintN argc, jsval *arglist, BOOL once)
+{
+	jsval	*argv=JS_ARGV(cx, arglist);
+	enum js_event_type et;
+	char operation[16];
+	size_t slen;
+
+	if (argc != 2) {
+		JS_ReportError(cx, "console.clearOn() and console.clearOnce() require exactly two parameters");
+		return JS_FALSE;
+	}
+	JSVALUE_TO_STRBUF(cx, argv[0], operation, sizeof(operation), &slen);
+	HANDLE_PENDING(cx, NULL);
+	if (strcmp(operation, "read") == 0) {
+		if (once)
+			et = JS_EVENT_CONSOLE_INPUT_ONCE;
+		else
+			et = JS_EVENT_CONSOLE_INPUT;
+	}
+	else {
+		JS_SET_RVAL(cx, arglist, JSVAL_VOID);
+		return JS_TRUE;
+	}
+
+	return js_clear_event(cx, argc, arglist, et);
+}
+
+static JSBool
+js_once(JSContext *cx, uintN argc, jsval *arglist)
+{
+	return js_install_event(cx, argc, arglist, TRUE);
+}
+
+static JSBool
+js_clearOnce(JSContext *cx, uintN argc, jsval *arglist)
+{
+	return js_clear_console_event(cx, argc, arglist, TRUE);
+}
+
+static JSBool
+js_on(JSContext *cx, uintN argc, jsval *arglist)
+{
+	return js_install_event(cx, argc, arglist, FALSE);
+}
+
+static JSBool
+js_clearOn(JSContext *cx, uintN argc, jsval *arglist)
+{
+	return js_clear_console_event(cx, argc, arglist, TRUE);
+}
+
 static jsSyncMethodSpec js_console_functions[] = {
 	{"inkey",			js_inkey,			0, JSTYPE_STRING,	JSDOCSTR("[mode=<tt>K_NONE</tt>] [,timeout=<tt>0</tt>]")
 	,JSDOCSTR("get a single key with optional <i>timeout</i> in milliseconds (defaults to 0, for no wait).<br>"
@@ -2362,6 +2507,22 @@ static jsSyncMethodSpec js_console_functions[] = {
 		,JSDOCSTR("scroll all current mouse hot-spots by the specific number of rows")
 		,31800
 		},
+	{"on",			js_on,				2,	JSTYPE_NUMBER,	JSDOCSTR("type, callback")
+		,JSDOCSTR("calls callback whenever the condition type specifies is possible.  Currently, only 'read' is supported for type.  Returns an id suitable for use with clearOn")
+		,31802
+		},
+	{"once",		js_once,			2,	JSTYPE_NUMBER,	JSDOCSTR("type, callback")
+		,JSDOCSTR("calls callback the first time the condition type specifies is possible.  Currently, only 'read' is supported for type.  Returns an id suitable for use with clearOnce")
+		,31802
+		},
+	{"clearOn",		js_clearOn,			2,	JSTYPE_VOID,	JSDOCSTR("type, id")
+		,JSDOCSTR("removes a callback installed by on")
+		,31802
+		},
+	{"clearOnce",		js_clearOnce,			2,	JSTYPE_VOID,	JSDOCSTR("type, id")
+		,JSDOCSTR("removes a callback installed by once")
+		,31802
+		},
 	{0}
 };
 
diff --git a/src/sbbs3/js_internal.c b/src/sbbs3/js_internal.c
index e13b8a5a9f9555813c3af44114568f114c2b774c..9027fa46b8b6ce3514b71d7ecac1e2d1df9919b5 100644
--- a/src/sbbs3/js_internal.c
+++ b/src/sbbs3/js_internal.c
@@ -20,7 +20,9 @@
  ****************************************************************************/
 
 #include "sbbs.h"
+#include "sockwrap.h"
 #include "js_request.h"
+#include "js_socket.h"
 
 /* SpiderMonkey: */
 #include <jsdbgapi.h>
@@ -42,6 +44,7 @@ enum {
 #endif
 	,PROP_GLOBAL
 	,PROP_OPTIONS
+	,PROP_KEEPGOING
 };
 
 static JSBool js_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
@@ -111,6 +114,12 @@ static JSBool js_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 		case PROP_OPTIONS:
 			*vp = UINT_TO_JSVAL(JS_GetOptions(cx));
 			break;
+		case PROP_KEEPGOING:
+			if (cb->events_supported)
+				*vp = BOOLEAN_TO_JSVAL(cb->keepGoing);
+			else
+				*vp = JSVAL_FALSE;
+			break;
 	}
 
 	return(JS_TRUE);
@@ -158,6 +167,10 @@ static JSBool js_set(JSContext *cx, JSObject *obj, jsid id, JSBool strict, jsval
 				return JS_FALSE;
 			break;
 #endif
+		case PROP_KEEPGOING:
+			if (cb->events_supported)
+				JS_ValueToBoolean(cx, *vp, &cb->keepGoing);
+			break;
 	}
 
 	return(JS_TRUE);
@@ -186,6 +199,7 @@ static jsSyncPropertySpec js_properties[] = {
 #endif
 	{	"global",			PROP_GLOBAL,		PROP_FLAGS,			314 },
 	{	"options",			PROP_OPTIONS,		PROP_FLAGS,			31802 },
+	{	"do_callbacks",		PROP_KEEPGOING,		JSPROP_ENUMERATE,			31802 },
 	{0}
 };
 
@@ -207,6 +221,7 @@ static char* prop_desc[] = {
 #endif
 	,"global (top level) object - <small>READ ONLY</small>"
 	,"option flags - <small>READ ONLY</small>"
+	,"do callbacks after script finishes running"
 	/* New properties go here... */
 	,"full path and filename of JS file executed"
 	,"directory of executed JS file"
@@ -690,6 +705,659 @@ static JSBool js_flatten(JSContext *cx, uintN argc, jsval *arglist)
 	return(JS_TRUE);
 }
 
+static JSBool
+js_setTimeout(JSContext *cx, uintN argc, jsval *arglist)
+{
+	jsval	*argv=JS_ARGV(cx, arglist);
+	struct js_event_list *ev;
+	js_callback_t*	cb;
+	JSObject *obj=JS_THIS_OBJECT(cx, arglist);
+	JSFunction *ecb;
+	uint64_t now = xp_timer() * 1000;
+	jsdouble timeout;
+
+	if((cb=(js_callback_t*)JS_GetPrivate(cx,obj))==NULL)
+		return(JS_FALSE);
+
+	if (!cb->events_supported) {
+		JS_ReportError(cx, "events not supported");
+		return JS_FALSE;
+	}
+
+	if (argc < 2) {
+		JS_ReportError(cx, "js.setTimeout() requires two parameters");
+		return JS_FALSE;
+	}
+	ecb = JS_ValueToFunction(cx, argv[0]);
+	if (ecb == NULL) {
+		return JS_FALSE;
+	}
+	if (argc > 2) {
+		if (!JS_ValueToObject(cx, argv[2], &obj))
+			return JS_FALSE;
+	}
+	if (!JS_ValueToNumber(cx, argv[1], &timeout)) {
+		return JS_FALSE;
+	}
+	ev = malloc(sizeof(*ev));
+	if (ev == NULL) {
+		JS_ReportError(cx, "error allocating %lu bytes", sizeof(*ev));
+		return JS_FALSE;
+	}
+	ev->prev = NULL;
+	ev->next = cb->events;
+	if (ev->next)
+		ev->next->prev = ev;
+	ev->type = JS_EVENT_TIMEOUT;
+	ev->cx = obj;
+	JS_AddObjectRoot(cx, &ev->cx);
+	ev->cb = ecb;
+	ev->data.timeout.end = now + timeout;
+	ev->id = cb->next_eid++;
+	cb->events = ev;
+
+	JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(ev->id));
+	return JS_TRUE;
+}
+
+JSBool
+js_clear_event(JSContext *cx, uintN argc, jsval *arglist, enum js_event_type et)
+{
+	jsval	*argv=JS_ARGV(cx, arglist);
+	int32 id;
+	js_callback_t*	cb;
+	struct js_event_list *ev;
+	struct js_event_list *nev;
+	JSObject *obj=JS_THIS_OBJECT(cx, arglist);
+
+	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
+
+	if (argc < 1) {
+		JS_ReportError(cx, "js.clearTimeout() requires an id");
+		return JS_FALSE;
+	}
+	if (!JS_ValueToInt32(cx, argv[0], &id)) {
+		return JS_FALSE;
+	}
+	if((cb=(js_callback_t*)JS_GetPrivate(cx, obj))==NULL)
+		return(JS_FALSE);
+	if (!cb->events_supported) {
+		JS_ReportError(cx, "events not supported");
+		return JS_FALSE;
+	}
+
+	for (ev = cb->events; ev; ev = nev) {
+		nev = ev->next;
+		if (ev->type == et && ev->id == id) {
+			if (ev->next)
+				ev->next->prev = ev->prev;
+			if (ev->prev)
+				ev->prev->next = ev->next;
+			else
+				cb->events = ev->next;
+			JS_RemoveObjectRoot(cx, &ev->cx);
+			free(ev);
+		}
+	}
+
+	return JS_TRUE;
+}
+
+static JSBool
+js_clearTimeout(JSContext *cx, uintN argc, jsval *arglist)
+{
+	return js_clear_event(cx, argc, arglist, JS_EVENT_TIMEOUT);
+}
+
+static JSBool
+js_clearInterval(JSContext *cx, uintN argc, jsval *arglist)
+{
+	return js_clear_event(cx, argc, arglist, JS_EVENT_INTERVAL);
+}
+
+static JSBool
+js_setInterval(JSContext *cx, uintN argc, jsval *arglist)
+{
+	jsval	*argv=JS_ARGV(cx, arglist);
+	struct js_event_list *ev;
+	js_callback_t*	cb;
+	JSObject *obj=JS_THIS_OBJECT(cx, arglist);
+	JSFunction *ecb;
+	uint64_t now = xp_timer() * 1000;
+	jsdouble period;
+
+	if((cb=(js_callback_t*)JS_GetPrivate(cx,obj))==NULL)
+		return(JS_FALSE);
+
+	if (!cb->events_supported) {
+		JS_ReportError(cx, "events not supported");
+		return JS_FALSE;
+	}
+
+	if (argc < 2) {
+		JS_ReportError(cx, "js.setInterval() requires two parameters");
+		return JS_FALSE;
+	}
+	ecb = JS_ValueToFunction(cx, argv[0]);
+	if (ecb == NULL) {
+		return JS_FALSE;
+	}
+	if (!JS_ValueToNumber(cx, argv[1], &period)) {
+		return JS_FALSE;
+	}
+	if (argc > 2) {
+		if (!JS_ValueToObject(cx, argv[2], &obj))
+			return JS_FALSE;
+	}
+	ev = malloc(sizeof(*ev));
+	if (ev == NULL) {
+		JS_ReportError(cx, "error allocating %lu bytes", sizeof(*ev));
+		return JS_FALSE;
+	}
+	ev->prev = NULL;
+	ev->next = cb->events;
+	if (ev->next)
+		ev->next->prev = ev;
+	ev->type = JS_EVENT_INTERVAL;
+	ev->cx = obj;
+	JS_AddObjectRoot(cx, &ev->cx);
+	ev->cb = ecb;
+	ev->data.interval.last = now;
+	ev->data.interval.period = period;
+	ev->id = cb->next_eid++;
+	cb->events = ev;
+
+	JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(ev->id));
+	return JS_TRUE;
+}
+
+static JSBool
+js_addEventListener(JSContext *cx, uintN argc, jsval *arglist)
+{
+	jsval	*argv=JS_ARGV(cx, arglist);
+	char *name;
+	JSFunction *cbf;
+	struct js_listener_entry *listener;
+	JSObject *obj=JS_THIS_OBJECT(cx, arglist);
+	js_callback_t *cb;
+
+	if((cb=(js_callback_t*)JS_GetPrivate(cx,obj))==NULL)
+		return(JS_FALSE);
+
+	if (argc < 2) {
+		JS_ReportError(cx, "js.addEventListener() requires exactly two parameters");
+		return JS_FALSE;
+	}
+
+	cbf = JS_ValueToFunction(cx, argv[1]);
+	if (cbf == NULL) {
+		return JS_FALSE;
+	}
+
+	JSVALUE_TO_MSTRING(cx, argv[0], name, NULL);
+	HANDLE_PENDING(cx, name);
+
+	listener = malloc(sizeof(*listener));
+	if (listener == NULL) {
+		free(name);
+		JS_ReportError(cx, "error allocating %ul bytes", sizeof(*listener));
+		return JS_FALSE;
+	}
+
+	listener->func = cbf;
+	listener->id = cb->next_eid++;
+	listener->next = cb->listeners;
+	listener->name = name;
+	cb->listeners = listener;
+
+	JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(listener->id));
+	return JS_TRUE;
+}
+
+static JSBool
+js_removeEventListener(JSContext *cx, uintN argc, jsval *arglist)
+{
+	jsval *argv=JS_ARGV(cx, arglist);
+	int32 id;
+	struct js_listener_entry *listener;
+	struct js_listener_entry **plistener;
+	char *name;
+	JSObject *obj=JS_THIS_OBJECT(cx, arglist);
+	js_callback_t *cb;
+
+	if((cb=(js_callback_t*)JS_GetPrivate(cx,obj))==NULL)
+		return(JS_FALSE);
+
+	if (argc < 1) {
+		JS_ReportError(cx, "js.removeEventListener() requires exactly one parameter");
+		return JS_FALSE;
+	}
+
+	if (JSVAL_IS_INT(argv[0])) {
+		// Remove by ID
+		if (!JS_ValueToInt32(cx, argv[0], &id))
+			return JS_FALSE;
+		for (listener = cb->listeners, plistener = &cb->listeners; listener; listener = (*plistener)->next) {
+			if (listener->id == id) {
+				(*plistener)->next = listener->next;
+				free(listener);
+			}
+			else
+				*plistener = listener;
+		}
+	}
+	else {
+		// Remove by name
+		JSVALUE_TO_MSTRING(cx, argv[0], name, NULL);
+		HANDLE_PENDING(cx, name);
+		for (listener = cb->listeners, plistener = &cb->listeners; listener; listener = (*plistener)->next) {
+			if (strcmp(listener->name, name) == 0) {
+				(*plistener)->next = listener->next;
+				free(listener);
+			}
+			else
+				*plistener = listener;
+		}
+		free(name);
+	}
+
+	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
+	return JS_TRUE;
+}
+
+static JSBool
+js_dispatchEvent(JSContext *cx, uintN argc, jsval *arglist)
+{
+	jsval *argv=JS_ARGV(cx, arglist);
+	struct js_listener_entry *listener;
+	struct js_runq_entry *rqe;
+	JSObject *obj=JS_THIS_OBJECT(cx, arglist);
+	char *name;
+	js_callback_t *cb;
+
+	if((cb=(js_callback_t*)JS_GetPrivate(cx,obj))==NULL)
+		return(JS_FALSE);
+
+	if (argc < 1) {
+		JS_ReportError(cx, "js.dispatchEvent() requires a event name");
+		return JS_FALSE;
+	}
+
+	if (argc > 1) {
+		if (!JSVAL_IS_OBJECT(argv[1])) {
+			JS_ReportError(cx, "second argument must be an object");
+			return JS_FALSE;
+		}
+		obj = JSVAL_TO_OBJECT(argv[1]);
+	}
+
+	JSVALUE_TO_MSTRING(cx, argv[0], name, NULL);
+	HANDLE_PENDING(cx, name);
+
+	for (listener = cb->listeners; listener; listener = listener->next) {
+		if (strcmp(name, listener->name) == 0) {
+			rqe = malloc(sizeof(*rqe));
+			if (rqe == NULL) {
+				JS_ReportError(cx, "error allocating %ul bytes", sizeof(*rqe));
+				free(name);
+				return JS_FALSE;
+			}
+			rqe->func = listener->func;
+			rqe->cx = obj;
+			JS_AddObjectRoot(cx, &rqe->cx);
+			rqe->next = NULL;
+			if (cb->rq_tail != NULL)
+				cb->rq_tail->next = rqe;
+			cb->rq_tail = rqe;
+			if (cb->rq_head == NULL)
+				cb->rq_head = rqe;
+		}
+	}
+	free(name);
+
+	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
+	return JS_TRUE;
+}
+
+static void
+js_clear_events(JSContext *cx, js_callback_t *cb)
+{
+	struct js_event_list *ev;
+	struct js_event_list *nev;
+	struct js_runq_entry *rq;
+	struct js_runq_entry *nrq;
+	struct js_listener_entry *le;
+	struct js_listener_entry *nle;
+
+	for (ev = cb->events, nev = NULL; ev; ev = nev) {
+		nev = ev->next;
+		JS_RemoveObjectRoot(cx, &ev->cx);
+		free(ev);
+	}
+	cb->next_eid = 0;
+	cb->events = NULL;
+
+	for (rq = cb->rq_head; rq; rq = nrq) {
+		nrq = rq->next;
+		JS_RemoveObjectRoot(cx, &rq->cx);
+		free(rq);
+	}
+	cb->rq_head = NULL;
+	cb->rq_tail = NULL;
+
+	for (le = cb->listeners; le; le = nle) {
+		nle = le->next;
+		free(le);
+	}
+	cb->listeners = NULL;
+}
+
+JSBool
+js_handle_events(JSContext *cx, js_callback_t *cb, volatile int *terminated)
+{
+	struct js_event_list **head = &cb->events;
+	int timeout;
+	int i;
+	uint64_t now;
+	struct js_event_list *ev;
+	struct js_event_list *tev;
+	struct js_event_list *cev;
+	jsval rval = JSVAL_NULL;
+	jsrefcount rc;
+	JSBool ret = JS_TRUE;
+	BOOL input_locked = FALSE;
+#ifdef PREFER_POLL
+	struct pollfd *fds;
+	nfds_t sc;
+	nfds_t cfd;
+#else
+	fd_set rfds;
+	fd_set wfds;
+	struct timeval tv;
+	SOCKET hsock;
+#endif
+
+	if (!cb->events_supported)
+		return JS_FALSE;
+
+	while (cb->keepGoing && !JS_IsExceptionPending(cx) && cb->events && !*terminated) {
+		timeout = -1;	// Infinity by default...
+		now = xp_timer() * 1000;
+		ev = NULL;
+		tev = NULL;
+		cev = NULL;
+#ifdef PREFER_POLL
+		fds = NULL;
+		sc = 0;
+		cfd = 0;
+#else
+		hsock = 0;
+#endif
+		
+#ifdef PREFER_POLL
+		for (ev = *head; ev; ev = ev->next) {
+			if (ev->type == JS_EVENT_SOCKET_READABLE || ev->type == JS_EVENT_SOCKET_READABLE_ONCE
+			    || ev->type == JS_EVENT_SOCKET_WRITABLE || ev->type == JS_EVENT_SOCKET_WRITABLE_ONCE
+			    || ev->type == JS_EVENT_SOCKET_CONNECT || ev->type == JS_EVENT_CONSOLE_INPUT
+			    || ev->type == JS_EVENT_CONSOLE_INPUT_ONCE)
+				sc++;
+		}
+
+		if (sc) {
+			fds = calloc(sc, sizeof(*fds));
+			if (fds == NULL) {
+				JS_ReportError(cx, "error allocating %d elements of %ul bytes", sc, sizeof(*fds));
+				return JS_FALSE;
+			}
+		}
+#else
+		FD_ZERO(&rfds);
+		FD_ZERO(&wfds);
+#endif
+
+		rc = JS_SUSPENDREQUEST(cx);
+		if (cb->rq_head)
+			timeout = 0;
+		for (ev = *head; ev; ev = ev->next) {
+			switch (ev->type) {
+				case JS_EVENT_SOCKET_READABLE_ONCE:
+				case JS_EVENT_SOCKET_READABLE:
+#ifdef PREFER_POLL
+					fds[cfd].fd = ev->data.sock;
+					fds[cfd].events = POLLIN;
+					cfd++;
+#else
+					FD_SET(ev->data.sock, &rfds);
+					if (ev->data.sock > hsock)
+						hsock = ev->data.sock;
+#endif
+					break;
+				case JS_EVENT_SOCKET_WRITABLE_ONCE:
+				case JS_EVENT_SOCKET_WRITABLE:
+#ifdef PREFER_POLL
+					fds[cfd].fd = ev->data.sock;
+					fds[cfd].events = POLLOUT;
+					cfd++;
+#else
+					FD_SET(ev->data.sock, &wfds);
+					if (ev->data.sock > hsock)
+						hsock = ev->data.sock;
+#endif
+					break;
+				case JS_EVENT_SOCKET_CONNECT:
+#ifdef PREFER_POLL
+					fds[cfd].fd = ev->data.connect.sv[0];
+					fds[cfd].events = POLLIN;
+					cfd++;
+#else
+					FD_SET(ev->data.connect.sv[0], &rfds);
+					if (ev->data.sock > hsock)
+						hsock = ev->data.sock;
+#endif
+					break;
+				case JS_EVENT_INTERVAL:
+					// TODO: Handle clock wrapping
+					if (ev->data.interval.last + ev->data.interval.period <= now) {
+						timeout = 0;
+						tev = ev;
+					}
+					else {
+						i = ev->data.interval.last + ev->data.interval.period - now;
+						if (timeout == -1 || i < timeout) {
+							timeout = i;
+							tev = ev;
+						}
+					}
+					break;
+				case JS_EVENT_TIMEOUT:
+					// TODO: Handle clock wrapping
+					if (now >= ev->data.timeout.end) {
+						timeout = 0;
+						tev = ev;
+					}
+					else {
+						i = ev->data.timeout.end - now;
+						if (timeout == -1 || i < timeout) {
+							timeout = i;
+							tev = ev;
+						}
+					}
+					break;
+				case JS_EVENT_CONSOLE_INPUT_ONCE:
+				case JS_EVENT_CONSOLE_INPUT:
+					if (!input_locked)
+						js_do_lock_input(cx, TRUE);
+					if (js_cx_input_pending(cx) != 0) {
+						js_do_lock_input(cx, FALSE);
+						timeout = 0;
+						cev = ev;
+					}
+					else {
+						input_locked = TRUE;
+#ifdef PREFER_POLL
+						fds[cfd].fd = ev->data.sock;
+						fds[cfd].events = POLLIN;
+						cfd++;
+#else
+						FD_SET(ev->data.sock, &rfds);
+						if (ev->data.sock > hsock)
+							hsock = ev->data.sock;
+#endif
+					}
+					break;
+			}
+		}
+
+		// TODO: suspend/resume request
+#ifdef PREFER_POLL
+		switch (poll(fds, cfd, timeout)) {
+#else
+		tv.tv_sec = timeout / 1000;
+		tv.tv_usec = (timeout % 1000) * 1000;
+		switch (select(hsock + 1, &rfds, &wfds, NULL, &tv)) {
+#endif
+			case 0:		// Timeout
+				JS_RESUMEREQUEST(cx, rc);
+				if (tev && tev->cx && tev->cb)
+					ev = tev;
+				else if (cev && cev->cx && cev->cb)
+					ev = cev;
+				else {
+					if (cb->rq_head == NULL) {
+						JS_ReportError(cx, "Timeout with no event!");
+						ret = JS_FALSE;
+						goto done;
+					}
+				}
+				break;
+			case -1:	// Error...
+				JS_RESUMEREQUEST(cx, rc);
+				if (ERROR_VALUE != EINTR) {
+					JS_ReportError(cx, "poll() returned with error %d", ERROR_VALUE);
+					ret = JS_FALSE;
+					goto done;
+				}
+				break;
+			default:	// Zero or more sockets ready
+				JS_RESUMEREQUEST(cx, rc);
+#ifdef PREFER_POLL
+				cfd = 0;
+#endif
+				for (ev = *head; ev; ev = ev->next) {
+					if (ev->type == JS_EVENT_SOCKET_READABLE || ev->type == JS_EVENT_SOCKET_READABLE_ONCE) {
+#ifdef PREFER_POLL
+						if (fds[cfd].revents & ~(POLLOUT | POLLWRNORM | POLLWRBAND)) {
+#else
+						if (FD_ISSET(ev->data.sock, &rfds)) {
+#endif
+							break;
+						}
+#ifdef PREFER_POLL
+						cfd++;
+#endif
+					}
+					else if (ev->type == JS_EVENT_SOCKET_WRITABLE || ev->type == JS_EVENT_SOCKET_WRITABLE_ONCE) {
+#ifdef PREFER_POLL
+						if (fds[cfd].revents & ~(POLLIN | POLLRDNORM | POLLRDBAND | POLLPRI)) {
+#else
+						if (FD_ISSET(ev->data.sock, &wfds)) {
+#endif
+							break;
+						}
+#ifdef PREFER_POLL
+						cfd++;
+#endif
+					}
+					else if (ev->type == JS_EVENT_SOCKET_CONNECT) {
+#ifdef PREFER_POLL
+						if (fds[cfd].revents & ~(POLLOUT | POLLWRNORM | POLLWRBAND)) {
+#else
+						if (FD_ISSET(ev->data.connect.sv[0], &wfds)) {
+#endif
+							closesocket(ev->data.connect.sv[0]);
+							break;
+						}
+#ifdef PREFER_POLL
+						cfd++;
+#endif
+					}
+					else if (ev->type == JS_EVENT_CONSOLE_INPUT) {
+#ifdef PREFER_POLL
+						if (fds[cfd].revents & ~(POLLOUT | POLLWRNORM | POLLWRBAND)) {
+#else
+						if (FD_ISSET(ev->data.sock, &wfds)) {
+#endif
+							break;
+						}
+#ifdef PREFER_POLL
+						cfd++;
+#endif
+					}
+				}
+				break;
+		}
+		if (ev == NULL) {
+			if (cb->rq_head == NULL) {
+				JS_ReportError(cx, "poll() returned ready, but didn't find anything");
+				ret = JS_FALSE;
+				goto done;
+			}
+			else {
+				struct js_runq_entry    *rqe = cb->rq_head;
+				cb->rq_head = rqe->next;
+				if (cb->rq_head == NULL)
+					cb->rq_tail = NULL;
+				ret = JS_CallFunction(cx, rqe->cx, rqe->func, 0, NULL, &rval);
+				JS_RemoveObjectRoot(cx, &rqe->cx);
+				free(rqe);
+			}
+		}
+		else {
+			// Deal with things before running the callback
+			if (ev->type == JS_EVENT_CONSOLE_INPUT) {
+				if (input_locked)
+					js_do_lock_input(cx, FALSE);
+			}
+
+			ret = JS_CallFunction(cx, ev->cx, ev->cb, 0, NULL, &rval);
+
+			// Clean up/update after call
+			if (ev->type == JS_EVENT_SOCKET_CONNECT) {
+				// TODO: call recv() and pass result to callback?
+				closesocket(ev->data.connect.sv[0]);
+			}
+			else if (ev->type == JS_EVENT_INTERVAL)
+				ev->data.interval.last += ev->data.interval.period;
+
+			// Remove one-shot events
+			if (ev->type == JS_EVENT_SOCKET_READABLE_ONCE || ev->type == JS_EVENT_SOCKET_WRITABLE_ONCE
+			    || ev->type == JS_EVENT_SOCKET_CONNECT || ev->type == JS_EVENT_TIMEOUT
+			    || ev->type == JS_EVENT_CONSOLE_INPUT_ONCE) {
+				if (ev->next)
+					ev->next->prev = ev->prev;
+				if (ev->prev)
+					ev->prev->next = ev->next;
+				else
+					*head = ev->next;
+				JS_RemoveObjectRoot(cx, &ev->cx);
+				free(ev);
+			}
+		}
+
+done:
+#ifdef PREFER_POLL
+		free(fds);
+#endif
+		if (input_locked)
+			js_do_lock_input(cx, FALSE);
+
+		if (!ret)
+			break;
+	}
+	js_clear_events(cx, cb);
+	return ret;
+}
+
 static jsSyncMethodSpec js_functions[] = {
 	{"eval",            js_eval,            0,	JSTYPE_UNDEF,	JSDOCSTR("script")
 	,JSDOCSTR("evaluate a JavaScript string in its own (secure) context, returning the result")
@@ -732,6 +1400,36 @@ static jsSyncMethodSpec js_functions[] = {
 	"NOTE: Use <tt>js.exec.apply()</tt> if you need to pass a variable number of arguments to the executed script.")
 	,31702
 	},
+	{"setTimeout",	js_setTimeout,			2,	JSTYPE_NUMBER,	JSDOCSTR("callback, time[, thisObj]")
+	,JSDOCSTR("install a timeout.  callback() will be called time ms after the function is called. "
+	 "returns an id which can be passed to clearTimeout()")
+	,31802
+	},
+	{"setInterval",	js_setInterval,			2,	JSTYPE_OBJECT,	JSDOCSTR("callback, period[, thisObj]")
+	,JSDOCSTR("install a timeout.  callback() will be called every period ms after setInterval() is called."
+	 "returns and id which can be passed to clearIntervat()")
+	,31802
+	},
+	{"clearTimeout",	js_clearTimeout,	1,	JSTYPE_VOID,	JSDOCSTR("id")
+	,JSDOCSTR("remove a timeout")
+	,31802
+	},
+	{"clearInterval",	js_clearInterval,	1,	JSTYPE_VOID,	JSDOCSTR("id")
+	,JSDOCSTR("remove an interval")
+	,31802
+	},
+	{"addEventListener",	js_addEventListener,	2,	JSTYPE_NUMBER,	JSDOCSTR("eventName, callback")
+	,JSDOCSTR("Add a listener that is ran after js.dispatchEvent(eventName) is called.  Returns an id to be passed to js.removeEventListener")
+	,31802
+	},
+	{"removeEventListener",	js_removeEventListener,	1,	JSTYPE_VOID,	JSDOCSTR("id")
+	,JSDOCSTR("Remove listeners added with js.addEventListener().  id can be a string or an id returned by addEventListener.  This does not remove already triggered callbacks from the runqueue.")
+	,31802
+	},
+	{"dispatchEvent",	js_dispatchEvent,	1,	JSTYPE_VOID,	JSDOCSTR("eventName [, thisObj]")
+	,JSDOCSTR("Add all listeners of eventName to the runqueue.  If obj is passed, specifies this in the callback (the js object is used otherwise).")
+	,31802
+	},
 	{0}
 };
 
diff --git a/src/sbbs3/js_socket.c b/src/sbbs3/js_socket.c
index e3f9ec7d1fe41a54a1d43d6a5e11070682e04048..862b9eef521306938d69a4e45dd87b6d8afc8346 100644
--- a/src/sbbs3/js_socket.c
+++ b/src/sbbs3/js_socket.c
@@ -33,7 +33,7 @@ static void dbprintf(BOOL error, js_socket_private_t* p, char* fmt, ...);
 static bool do_CryptFlush(js_socket_private_t *p);
 static int do_cryptAttribute(const CRYPT_CONTEXT session, CRYPT_ATTRIBUTE_TYPE attr, int val);
 static int do_cryptAttributeString(const CRYPT_CONTEXT session, CRYPT_ATTRIBUTE_TYPE attr, void *val, int len);
-static void do_js_close(js_socket_private_t *p, bool finalize);
+static void do_js_close(JSContext *cx, js_socket_private_t *p, bool finalize);
 static BOOL js_DefineSocketOptionsArray(JSContext *cx, JSObject *obj, int type);
 static JSBool js_accept(JSContext *cx, uintN argc, jsval *arglist);
 static JSBool js_bind(JSContext *cx, uintN argc, jsval *arglist);
@@ -42,6 +42,7 @@ static JSBool js_connect(JSContext *cx, uintN argc, jsval *arglist);
 static void js_finalize_socket(JSContext *cx, JSObject *obj);
 static JSBool js_ioctlsocket(JSContext *cx, uintN argc, jsval *arglist);
 static JSBool js_listen(JSContext *cx, uintN argc, jsval *arglist);
+static js_callback_t * js_get_callback(JSContext *cx);
 static JSBool js_getsockopt(JSContext *cx, uintN argc, jsval *arglist);
 static JSBool js_peek(JSContext *cx, uintN argc, jsval *arglist);
 static JSBool js_poll(JSContext *cx, uintN argc, jsval *arglist);
@@ -59,13 +60,16 @@ static JSBool js_setsockopt(JSContext *cx, uintN argc, jsval *arglist);
 static int js_sock_read_check(js_socket_private_t *p, time_t start, int32 timeout, int i);
 static JSBool js_socket_constructor(JSContext *cx, uintN argc, jsval *arglist);
 static JSBool js_socket_enumerate(JSContext *cx, JSObject *obj);
-static BOOL js_socket_peek_byte(js_socket_private_t *p);
+static BOOL js_socket_peek_byte(JSContext *cx, js_socket_private_t *p);
 static JSBool js_socket_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp);
-static ptrdiff_t js_socket_recv(js_socket_private_t *p, void *buf, size_t len, int flags, int timeout);
+static ptrdiff_t js_socket_recv(JSContext *cx, js_socket_private_t *p, void *buf, size_t len, int flags, int timeout);
 static JSBool js_socket_resolve(JSContext *cx, JSObject *obj, jsid id);
 static int js_socket_sendfilesocket(js_socket_private_t *p, int file, off_t *offset, off_t count);
 static ptrdiff_t js_socket_sendsocket(js_socket_private_t *p, const void *msg, size_t len, int flush);
 static JSBool js_socket_set(JSContext *cx, JSObject *obj, jsid id, JSBool strict, jsval *vp);
+static JSBool js_install_event(JSContext *cx, uintN argc, jsval *arglist, BOOL once);
+static JSBool js_on(JSContext *cx, uintN argc, jsval *arglist);
+static JSBool js_once(JSContext *cx, uintN argc, jsval *arglist);
 
 static int do_cryptAttribute(const CRYPT_CONTEXT session, CRYPT_ATTRIBUTE_TYPE attr, int val)
 {
@@ -153,12 +157,70 @@ static bool do_CryptFlush(js_socket_private_t *p)
 	return true;
 }
 
-static void do_js_close(js_socket_private_t *p, bool finalize)
+static void
+remove_js_socket_event(JSContext *cx, js_callback_t *cb, SOCKET sock)
 {
+	struct js_event_list *ev;
+	struct js_event_list *nev;
+
+	if (!cb->events_supported) {
+		return;
+	}
+
+	for (ev = cb->events; ev; ev = nev) {
+		nev = ev->next;
+		if (ev->type == JS_EVENT_SOCKET_READABLE || ev->type == JS_EVENT_SOCKET_READABLE_ONCE
+		    || ev->type == JS_EVENT_SOCKET_READABLE || ev->type == JS_EVENT_SOCKET_READABLE_ONCE) {
+			if (ev->data.sock == sock) {
+				if (ev->next)
+					ev->next->prev = ev->prev;
+				if (ev->prev)
+					ev->prev->next = ev->next;
+				else
+					cb->events = ev->next;
+				JS_RemoveObjectRoot(cx, &ev->cx);
+				free(ev);
+			}
+		}
+		else if(ev->type == JS_EVENT_SOCKET_CONNECT) {
+			if (ev->data.connect.sock == sock) {
+				if (ev->next)
+					ev->next->prev = ev->prev;
+				if (ev->prev)
+					ev->prev->next = ev->next;
+				else
+					cb->events = ev->next;
+				closesocket(ev->data.connect.sv[0]);
+				JS_RemoveObjectRoot(cx, &ev->cx);
+				free(ev);
+			}
+		}
+	}
+}
+
+static void do_js_close(JSContext *cx, js_socket_private_t *p, bool finalize)
+{
+	size_t i;
+
 	if(p->session != -1) {
 		cryptDestroySession(p->session);
 		p->session=-1;
 	}
+
+	// Delete any event handlers for the socket
+	if (p->js_cb) {
+		if (p->set) {
+			for (i = 0; i < p->set->sock_count; i++) {
+				if (p->set->socks[i].sock != INVALID_SOCKET)
+					remove_js_socket_event(cx, p->js_cb, p->set->socks[i].sock);
+			}
+		}
+		else {
+			if (p->sock != INVALID_SOCKET)
+				remove_js_socket_event(cx, p->js_cb, p->sock);
+		}
+	}
+
 	if(p->sock==INVALID_SOCKET) {
 		p->is_connected = FALSE;
 		return;
@@ -171,18 +233,19 @@ static void do_js_close(js_socket_private_t *p, bool finalize)
 		if (!finalize)
 			shutdown(p->sock, SHUT_RDWR);
 	}
+
 	// This is a lie for external sockets... don't tell anyone.
 	p->sock = INVALID_SOCKET;
 	p->is_connected = FALSE;
 }
 
-static BOOL js_socket_peek_byte(js_socket_private_t *p)
+static BOOL js_socket_peek_byte(JSContext *cx, js_socket_private_t *p)
 {
 	if (do_cryptAttribute(p->session, CRYPT_OPTION_NET_READTIMEOUT, 0) != CRYPT_OK)
 		return FALSE;
 	if (p->peeked)
 		return TRUE;
-	if (js_socket_recv(p, &p->peeked_byte, 1, 0, 0) == 1) {
+	if (js_socket_recv(cx, p, &p->peeked_byte, 1, 0, 0) == 1) {
 		p->peeked = TRUE;
 		return TRUE;
 	}
@@ -192,7 +255,7 @@ static BOOL js_socket_peek_byte(js_socket_private_t *p)
 /* Returns > 0 upon successful data received (even if there was an error or disconnection) */
 /* Returns -1 upon error (and no data received) */
 /* Returns 0 upon timeout or disconnection (and no data received) */
-static ptrdiff_t js_socket_recv(js_socket_private_t *p, void *buf, size_t len, int flags, int timeout)
+static ptrdiff_t js_socket_recv(JSContext *cx, js_socket_private_t *p, void *buf, size_t len, int flags, int timeout)
 {
 	ptrdiff_t	total=0;
 	int	copied,ret;
@@ -204,7 +267,7 @@ static ptrdiff_t js_socket_recv(js_socket_private_t *p, void *buf, size_t len, i
 		return total;
 	if (p->session != -1) {
 		if (flags & MSG_PEEK)
-			js_socket_peek_byte(p);
+			js_socket_peek_byte(cx, p);
 		if (p->peeked) {
 			*(uint8_t *)buf = p->peeked_byte;
 			buf=((uint8_t *)buf) + 1;
@@ -240,7 +303,7 @@ static ptrdiff_t js_socket_recv(js_socket_private_t *p, void *buf, size_t len, i
 					ret = 0;
 				else if (status != CRYPT_ERROR_COMPLETE) {
 					GCES(ret, p, estr, "popping data");
-					do_js_close(p, false);
+					do_js_close(cx, p, false);
 				}
 			}
 		}
@@ -392,7 +455,7 @@ static void js_finalize_socket(JSContext *cx, JSObject *obj)
 	if((p=(js_socket_private_t*)JS_GetPrivate(cx,obj))==NULL)
 		return;
 
-	do_js_close(p, true);
+	do_js_close(cx, p, true);
 
 	if(!p->external)
 		free(p->set);
@@ -420,7 +483,7 @@ js_close(JSContext *cx, uintN argc, jsval *arglist)
 	}
 
 	rc=JS_SUSPENDREQUEST(cx);
-	do_js_close(p, false);
+	do_js_close(cx, p, false);
 	dbprintf(FALSE, p, "closed");
 	JS_RESUMEREQUEST(cx, rc);
 
@@ -809,6 +872,144 @@ js_accept(JSContext *cx, uintN argc, jsval *arglist)
 	return(JS_TRUE);
 }
 
+struct js_connect_event_args {
+	SOCKET sv[2];
+	SOCKET sock;
+	int socktype;
+	char *host;
+	BOOL nonblocking;
+	ushort port;
+};
+
+static void
+js_connect_event_thread(void *args)
+{
+	struct js_connect_event_args *a = args;
+	struct addrinfo	hints,*res,*cur;
+	int result;
+	ulong val;
+	char sresult;
+
+	SetThreadName("sbbs/jsConnect");
+	memset(&hints, 0, sizeof(hints));
+	hints.ai_socktype = a->socktype;
+	hints.ai_flags = AI_ADDRCONFIG;
+	result = getaddrinfo(a->host, NULL, &hints, &res);
+	if(result != 0)
+		goto done;
+	/* always set to blocking here */
+	val = 0;
+	ioctlsocket(a->sock, FIONBIO, &val);
+	result = SOCKET_ERROR;
+	for(cur=res; cur != NULL; cur=cur->ai_next) {
+		inet_setaddrport((void *)cur->ai_addr, a->port);
+
+		result = connect(a->sock, cur->ai_addr, cur->ai_addrlen);
+		if(result == 0)
+			break;
+	}
+	sresult = result;
+	/* Restore original setting here */
+	ioctlsocket(a->sock,FIONBIO,(ulong*)&(a->nonblocking));
+	send(a->sv[1], &sresult, 1, 0);
+
+done:
+	closesocket(a->sv[1]);
+	free(a);
+}
+
+static JSBool
+js_connect_event(JSContext *cx, uintN argc, jsval *arglist, js_socket_private_t *p, ushort port, JSObject *obj)
+{
+	SOCKET sv[2];
+	struct js_event_list *ev;
+	JSFunction *ecb;
+	js_callback_t *cb = js_get_callback(cx);
+	struct js_connect_event_args *args;
+	jsval *argv=JS_ARGV(cx, arglist);
+
+	if (p->sock == INVALID_SOCKET) {
+		JS_ReportError(cx, "invalid socket");
+		return JS_FALSE;
+	}
+
+	if (cb == NULL) {
+		return JS_FALSE;
+	}
+
+	if (!cb->events_supported) {
+		JS_ReportError(cx, "events not supported");
+		return JS_FALSE;
+	}
+
+	ecb = JS_ValueToFunction(cx, argv[2]);
+	if (ecb == NULL) {
+		return JS_FALSE;
+	}
+
+	// Create socket pair...
+#ifdef _WIN32
+	if (socketpair(AF_INET, SOCK_STREAM, 0, sv) == -1) {
+#else
+	if (socketpair(PF_UNIX, SOCK_STREAM, 0, sv) == -1) {
+#endif
+		JS_ReportError(cx, "Error %d creating socket pair", ERROR_VALUE);
+		return JS_FALSE;
+	}
+
+	// Create event
+	ev = malloc(sizeof(*ev));
+	if (ev == NULL) {
+		JS_ReportError(cx, "error allocating %lu bytes", sizeof(*ev));
+		closesocket(sv[0]);
+		closesocket(sv[1]);
+		return JS_FALSE;
+	}
+	ev->prev = NULL;
+	ev->next = cb->events;
+	if (ev->next)
+		ev->next->prev = ev;
+	ev->type = JS_EVENT_SOCKET_CONNECT;
+	ev->cx = obj;
+	JS_AddObjectRoot(cx, &ev->cx);
+	ev->cb = ecb;
+	ev->data.connect.sv[0] = sv[0];
+	ev->data.connect.sv[1] = sv[1];
+	ev->data.sock = p->sock;
+	ev->id = cb->next_eid++;
+	p->js_cb = cb;
+	cb->events = ev;
+
+	// Start thread
+	args = malloc(sizeof(*args));
+	if (args == NULL) {
+		JS_ReportError(cx, "error allocating %lu bytes", sizeof(*args));
+		closesocket(sv[0]);
+		closesocket(sv[1]);
+		return JS_FALSE;
+	}
+	args->sv[0] = sv[0];
+	args->sv[1] = sv[1];
+	args->sock = p->sock;
+	args->socktype = p->type;
+	args->host = strdup(p->hostname);
+	args->port = port;
+	args->nonblocking = p->nonblocking;
+	if (args->host == NULL) {
+		JS_ReportError(cx, "error duplicating hostname");
+		closesocket(sv[0]);
+		closesocket(sv[1]);
+		free(args);
+		return JS_FALSE;
+	}
+	_beginthread(js_connect_event_thread, 0 /* Can be smaller... */, args);
+
+	// Success!
+	p->is_connected = TRUE;
+	JS_SET_RVAL(cx, arglist, JSVAL_TRUE);
+	return JS_TRUE;
+}
+
 static JSBool
 js_connect(JSContext *cx, uintN argc, jsval *arglist)
 {
@@ -834,6 +1035,13 @@ js_connect(JSContext *cx, uintN argc, jsval *arglist)
 	JSSTRING_TO_MSTRING(cx, str, p->hostname, NULL);
 	port = js_port(cx,argv[1],p->type);
 	rc=JS_SUSPENDREQUEST(cx);
+
+	if (argc > 2 && JSVAL_IS_OBJECT(argv[2]) && JS_ObjectIsFunction(cx, JSVAL_TO_OBJECT(argv[2]))) {
+		JSBool bgr = js_connect_event(cx, argc, arglist, p, port, obj);
+		JS_RESUMEREQUEST(cx, rc);
+		return bgr;
+	}
+
 	dbprintf(FALSE, p, "resolving hostname: %s", p->hostname);
 
 	memset(&hints, 0, sizeof(hints));
@@ -1201,7 +1409,7 @@ js_recv(JSContext *cx, uintN argc, jsval *arglist)
 	}
 
 	rc=JS_SUSPENDREQUEST(cx);
-	len = js_socket_recv(p,buf,len,0,timeout);
+	len = js_socket_recv(cx,p,buf,len,0,timeout);
 	JS_RESUMEREQUEST(cx, rc);
 	if(len<0) {
 		p->last_error=ERROR_VALUE;
@@ -1386,7 +1594,7 @@ js_peek(JSContext *cx, uintN argc, jsval *arglist)
 	}
 	rc=JS_SUSPENDREQUEST(cx);
 	if(p->session==-1)
-		len = js_socket_recv(p,buf,len,MSG_PEEK,120);
+		len = js_socket_recv(cx, p,buf,len,MSG_PEEK,120);
 	else
 		len=0;
 	JS_RESUMEREQUEST(cx, rc);
@@ -1494,7 +1702,7 @@ js_recvline(JSContext *cx, uintN argc, jsval *arglist)
 			}
 		}
 
-		if((got=js_socket_recv(p, &ch, 1, 0, i?1:timeout))!=1) {
+		if((got=js_socket_recv(cx, p, &ch, 1, 0, i?1:timeout))!=1) {
 			if(p->session == -1)
 				p->last_error = ERROR_VALUE;
 			if (i == 0) {			// no data received
@@ -1555,18 +1763,18 @@ js_recvbin(JSContext *cx, uintN argc, jsval *arglist)
 	rc=JS_SUSPENDREQUEST(cx);
 	switch(size) {
 		case sizeof(BYTE):
-			if((rd=js_socket_recv(p,&b,size,MSG_WAITALL,120))==size)
+			if((rd=js_socket_recv(cx, p,&b,size,MSG_WAITALL,120))==size)
 				JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(b));
 			break;
 		case sizeof(WORD):
-			if((rd=js_socket_recv(p,(BYTE*)&w,size,MSG_WAITALL,120))==size) {
+			if((rd=js_socket_recv(cx, p,(BYTE*)&w,size,MSG_WAITALL,120))==size) {
 				if(p->network_byte_order)
 					w=ntohs(w);
 				JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(w));
 			}
 			break;
 		case sizeof(DWORD):
-			if((rd=js_socket_recv(p,(BYTE*)&l,size,MSG_WAITALL,120))==size) {
+			if((rd=js_socket_recv(cx, p,(BYTE*)&l,size,MSG_WAITALL,120))==size) {
 				if(p->network_byte_order)
 					l=ntohl(l);
 				JS_SET_RVAL(cx, arglist, UINT_TO_JSVAL(l));
@@ -1821,6 +2029,179 @@ js_poll(JSContext *cx, uintN argc, jsval *arglist)
 	return(JS_TRUE);
 }
 
+static js_callback_t *
+js_get_callback(JSContext *cx)
+{
+	JSObject*	scope = JS_GetScopeChain(cx);
+	JSObject*	pscope;
+	jsval val = JSVAL_NULL;
+	JSObject *pjs_obj;
+
+	pscope = scope;
+	while ((!JS_LookupProperty(cx, pscope, "js", &val) || val==JSVAL_VOID || !JSVAL_IS_OBJECT(val)) && pscope != NULL) {
+		pscope = JS_GetParent(cx, pscope);
+		if (pscope == NULL) {
+			JS_ReportError(cx, "Walked to global, no js object!");
+			return NULL;
+		}
+	}
+	pjs_obj = JSVAL_TO_OBJECT(val);
+	return JS_GetPrivate(cx, pjs_obj);
+}
+
+static void
+js_install_one_socket_event(JSContext *cx, JSObject *obj, JSFunction *ecb, js_callback_t *cb, js_socket_private_t *p, SOCKET sock, enum js_event_type et)
+{
+	struct js_event_list *ev;
+
+	ev = malloc(sizeof(*ev));
+	if (ev == NULL) {
+		JS_ReportError(cx, "error allocating %lu bytes", sizeof(*ev));
+		return;
+	}
+	ev->prev = NULL;
+	ev->next = cb->events;
+	if (ev->next)
+		ev->next->prev = ev;
+	ev->type = et;
+	ev->cx = obj;
+	JS_AddObjectRoot(cx, &ev->cx);
+	ev->cb = ecb;
+	ev->data.sock = sock;
+	ev->id = cb->next_eid;
+	p->js_cb = cb;
+	cb->events = ev;
+}
+
+static JSBool
+js_install_event(JSContext *cx, uintN argc, jsval *arglist, BOOL once)
+{
+	jsval	*argv=JS_ARGV(cx, arglist);
+	js_callback_t*	cb;
+	JSObject *obj=JS_THIS_OBJECT(cx, arglist);
+	JSFunction *ecb;
+	js_socket_private_t*	p;
+	size_t i;
+	char operation[16];
+	enum js_event_type et;
+	size_t slen;
+
+	if((p=(js_socket_private_t*)js_GetClassPrivate(cx, obj, &js_socket_class))==NULL) {
+		return(JS_FALSE);
+	}
+
+	if (argc != 2) {
+		JS_ReportError(cx, "js.on() and js.once() require exactly two parameters");
+		return JS_FALSE;
+	}
+	ecb = JS_ValueToFunction(cx, argv[1]);
+	if (ecb == NULL) {
+		return JS_FALSE;
+	}
+	JSVALUE_TO_STRBUF(cx, argv[0], operation, sizeof(operation), &slen);
+	HANDLE_PENDING(cx, NULL);
+	if (strcmp(operation, "read") == 0) {
+		if (once)
+			et = JS_EVENT_SOCKET_READABLE_ONCE;
+		else
+			et = JS_EVENT_SOCKET_READABLE;
+	}
+	else if (strcmp(operation, "write") == 0) {
+		if (once)
+			et = JS_EVENT_SOCKET_WRITABLE_ONCE;
+		else
+			et = JS_EVENT_SOCKET_WRITABLE;
+	}
+	else {
+		JS_ReportError(cx, "event parameter must be 'read' or 'write'");
+		return JS_FALSE;
+	}
+
+	cb = js_get_callback(cx);
+	if (cb == NULL) {
+		return JS_FALSE;
+	}
+	if (!cb->events_supported) {
+		JS_ReportError(cx, "events not supported");
+		return JS_FALSE;
+	}
+
+
+	if (p->set) {
+		for (i = 0; i < p->set->sock_count; i++) {
+			if (p->set->socks[i].sock != INVALID_SOCKET)
+				js_install_one_socket_event(cx, obj, ecb, cb, p, p->set->socks[i].sock, et);
+		}
+	}
+	else {
+		if (p->sock != INVALID_SOCKET)
+			js_install_one_socket_event(cx, obj, ecb, cb, p, p->sock, et);
+	}
+	JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(cb->next_eid));
+	cb->next_eid++;
+
+	if (JS_IsExceptionPending(cx))
+		return JS_FALSE;
+	return JS_TRUE;
+}
+
+static JSBool
+js_clear_socket_event(JSContext *cx, uintN argc, jsval *arglist, BOOL once)
+{
+	jsval	*argv=JS_ARGV(cx, arglist);
+	enum js_event_type et;
+	char operation[16];
+	size_t slen;
+
+	if (argc != 2) {
+		JS_ReportError(cx, "js.clearOn() and js.clearOnce() require exactly two parameters");
+		return JS_FALSE;
+	}
+	JSVALUE_TO_STRBUF(cx, argv[0], operation, sizeof(operation), &slen);
+	HANDLE_PENDING(cx, NULL);
+	if (strcmp(operation, "read") == 0) {
+		if (once)
+			et = JS_EVENT_SOCKET_READABLE_ONCE;
+		else
+			et = JS_EVENT_SOCKET_READABLE;
+	}
+	else if (strcmp(operation, "write") == 0) {
+		if (once)
+			et = JS_EVENT_SOCKET_WRITABLE_ONCE;
+		else
+			et = JS_EVENT_SOCKET_WRITABLE;
+	}
+	else {
+		JS_SET_RVAL(cx, arglist, JSVAL_VOID);
+		return JS_TRUE;
+	}
+
+	return js_clear_event(cx, argc, arglist, et);
+}
+
+static JSBool
+js_once(JSContext *cx, uintN argc, jsval *arglist)
+{
+	return js_install_event(cx, argc, arglist, TRUE);
+}
+
+static JSBool
+js_clearOnce(JSContext *cx, uintN argc, jsval *arglist)
+{
+	return js_clear_socket_event(cx, argc, arglist, TRUE);
+}
+
+static JSBool
+js_on(JSContext *cx, uintN argc, jsval *arglist)
+{
+	return js_install_event(cx, argc, arglist, FALSE);
+}
+
+static JSBool
+js_clearOn(JSContext *cx, uintN argc, jsval *arglist)
+{
+	return js_clear_socket_event(cx, argc, arglist, TRUE);
+}
 
 /* Socket Object Properties */
 enum {
@@ -1994,7 +2375,7 @@ static JSBool js_socket_set(JSContext *cx, JSObject *obj, jsid id, JSBool strict
 							cryptDestroySession(p->session);
 						p->session=-1;
 						ioctlsocket(p->sock,FIONBIO,(ulong*)&(p->nonblocking));
-						do_js_close(p, false);
+						do_js_close(cx, p, false);
 					}
 				}
 			}
@@ -2003,7 +2384,7 @@ static JSBool js_socket_set(JSContext *cx, JSObject *obj, jsid id, JSBool strict
 					cryptDestroySession(p->session);
 					p->session=-1;
 					ioctlsocket(p->sock,FIONBIO,(ulong*)&(p->nonblocking));
-					do_js_close(p, false);
+					do_js_close(cx, p, false);
 				}
 			}
 			JS_RESUMEREQUEST(cx, rc);
@@ -2069,7 +2450,7 @@ static JSBool js_socket_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 				if (p->peeked)
 					rd = TRUE;
 				else if (p->session != -1)
-					rd = js_socket_peek_byte(p);
+					rd = js_socket_peek_byte(cx, p);
 				else
 					socket_check(p->sock,&rd,NULL,0);
 			}
@@ -2082,7 +2463,7 @@ static JSBool js_socket_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 			}
 			cnt=0;
 			if (p->session != -1) {
-				if (js_socket_peek_byte(p))
+				if (js_socket_peek_byte(cx, p))
 					*vp=DOUBLE_TO_JSVAL((double)1);
 				else
 					*vp = JSVAL_ZERO;
@@ -2296,6 +2677,22 @@ static jsSyncMethodSpec js_socket_functions[] = {
 	"default timeout value is 0.0 seconds (immediate timeout)")
 	,310
 	},
+	{"on",		js_on,		2,	JSTYPE_NUMBER,	JSDOCSTR("('read' | 'write'), callback")
+	,JSDOCSTR("execute callback whenever socket is readable/writable.  Returns an id to be passed to js.clearOn()")
+	,31802
+	},
+	{"once",	js_once,	2,	JSTYPE_NUMBER,	JSDOCSTR("('read' | 'write'), callback")
+	,JSDOCSTR("execute callback next time socket is readable/writable  Returns and id to be passed to js.clearOnce()")
+	,31802
+	},
+	{"clearOn",	js_clearOn,	2,	JSTYPE_NUMBER,	JSDOCSTR("('read' | 'write'), id")
+	,JSDOCSTR("remove callback installed by Socket.on()")
+	,31802
+	},
+	{"clearOnce",	js_clearOnce,	2,	JSTYPE_NUMBER,	JSDOCSTR("('read' | 'write'), id")
+	,JSDOCSTR("remove callback installed by Socket.once()")
+	,31802
+	},
 	{0}
 };
 
diff --git a/src/sbbs3/js_socket.h b/src/sbbs3/js_socket.h
index 2944f252b8e86e81c41baa052aa4ab6e1d20d79e..8e5acce615d6cc0bed466172d073a9c8f8cc4142 100644
--- a/src/sbbs3/js_socket.h
+++ b/src/sbbs3/js_socket.h
@@ -25,6 +25,7 @@ typedef struct
 	char	peeked_byte;
 	BOOL	peeked;
 	uint16_t local_port;
+	js_callback_t *js_cb;
 } js_socket_private_t;
 
 #ifdef __cplusplus
diff --git a/src/sbbs3/js_system.c b/src/sbbs3/js_system.c
index da0d04e140f19e4bcfc04fd26497a0894d0ad781..d5583c9a868da703cf3e3414a1831351655e8ad2 100644
--- a/src/sbbs3/js_system.c
+++ b/src/sbbs3/js_system.c
@@ -103,8 +103,9 @@ enum {
 	,SYS_PROP_TEMP_PATH
 	,SYS_PROP_CMD_SHELL
 
-	/* last */
 	,SYS_PROP_LOCAL_HOSTNAME
+	/* last */
+	,SYS_PROP_NAME_SERVERS
 };
 
 static JSBool js_system_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
@@ -116,6 +117,10 @@ static JSBool js_system_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 	JSString*	js_str;
 	ulong		val;
 	jsrefcount	rc;
+	JSObject *robj;
+	jsval jval;
+	str_list_t list;
+	int i;
 
 	js_system_private_t* sys;
 	if((sys = (js_system_private_t*)js_GetClassPrivate(cx,obj,&js_system_class))==NULL)
@@ -321,6 +326,23 @@ static JSBool js_system_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 			JS_RESUMEREQUEST(cx, rc);
 			p=str;
 			break;
+		case SYS_PROP_NAME_SERVERS:
+			rc=JS_SUSPENDREQUEST(cx);
+			robj = JS_NewArrayObject(cx, 0, NULL);
+			if (robj == NULL)
+				return JS_FALSE;
+			*vp = OBJECT_TO_JSVAL(robj);
+			list = getNameServerList();
+			if (list != NULL) {
+				for (i = 0; list[i]; i++) {
+					jval = STRING_TO_JSVAL(JS_NewStringCopyZ(cx, list[i]));
+					if (!JS_SetElement(cx, robj, i, &jval))
+						break;
+				}
+			}
+			freeNameServerList(list);
+			JS_RESUMEREQUEST(cx, rc);
+			break;
 	}
 
 	if(p!=NULL) {	/* string property */
@@ -440,8 +462,9 @@ static jsSyncPropertySpec js_system_properties[] = {
 	{	"clock_ticks_per_second",	SYS_PROP_CLOCK_PER_SEC	,SYSOBJ_FLAGS,	311  },
 	{	"timer",					SYS_PROP_TIMER			,SYSOBJ_FLAGS,	314	 },
 
-	/* last */
 	{	"local_host_name",			SYS_PROP_LOCAL_HOSTNAME	,SYSOBJ_FLAGS,	311  },
+	{	"name_servers",			SYS_PROP_NAME_SERVERS,SYSOBJ_FLAGS,	31802  },
+	/* last */
 	{0}
 };
 
@@ -515,8 +538,9 @@ static char* sys_prop_desc[] = {
 	,"number of clock ticks per second"
 	,"high-resolution timer, in seconds (fractional seconds supported)"
 
-	/* INSERT new tabled properties here */
 	,"private host name that uniquely identifies this system on the local network"
+	,"array of nameservers in use by the system"
+	/* INSERT new tabled properties here */
 
 	/* Manually created (non-tabled) properties */
 	,"public host name that uniquely identifies this system on the Internet (usually the same as <i>system.inet_addr</i>)"
diff --git a/src/sbbs3/jsdoor.c b/src/sbbs3/jsdoor.c
index 92bff87b97f688ff51077d9858f4aaa1ab0571ed..d709c5514a5da449a7c059f87266032fee197b09 100644
--- a/src/sbbs3/jsdoor.c
+++ b/src/sbbs3/jsdoor.c
@@ -53,6 +53,16 @@
 
 scfg_t		scfg;
 
+void js_do_lock_input(JSContext *cx, JSBool lock)
+{
+	return;
+}
+
+size_t js_cx_input_pending(JSContext *cx)
+{
+	return 0;
+}
+
 void* DLLCALL js_GetClassPrivate(JSContext *cx, JSObject *obj, JSClass* cls)
 {
 	void *ret = JS_GetInstancePrivate(cx, obj, cls, NULL);
diff --git a/src/sbbs3/jsexec.c b/src/sbbs3/jsexec.c
index e60885ea196ada646f2554b636a39ceff0ae6978..ff7a96a6b17dd2c42462f1cbb8bcbbb0ea67a2e9 100644
--- a/src/sbbs3/jsexec.c
+++ b/src/sbbs3/jsexec.c
@@ -1078,7 +1078,11 @@ long js_exec(const char *fname, const char* buf, char** args)
 	if (abort) {
 		result = EXIT_FAILURE;
 	} else {
+		cb.keepGoing = FALSE;
+		cb.events_supported = TRUE;
 		exec_result = JS_ExecuteScript(js_cx, js_glob, js_script, &rval);
+		js_handle_events(js_cx, &cb, &terminated);
+
 		char	*p;
 		if(buf != NULL) {
 			JSVALUE_TO_MSTRING(js_cx, rval, p, NULL);
@@ -1213,6 +1217,7 @@ int main(int argc, char **argv, char** env)
 	cb.yield_interval=JAVASCRIPT_YIELD_INTERVAL;
 	cb.gc_interval=JAVASCRIPT_GC_INTERVAL;
 	cb.auto_terminate=TRUE;
+	cb.events = NULL;
 
 	DESCRIBE_COMPILER(compiler);
 
diff --git a/src/sbbs3/main.cpp b/src/sbbs3/main.cpp
index 090ef481e1ae3f6ad5d23ec855e6203854aacb8f..ed2d051a45ff276bf89875fe4f21becff2dc0383 100644
--- a/src/sbbs3/main.cpp
+++ b/src/sbbs3/main.cpp
@@ -1285,6 +1285,7 @@ JSContext* sbbs_t::js_init(JSRuntime** runtime, JSObject** glob, const char* des
 	js_callback.yield_interval = startup->js.yield_interval;
 	js_callback.terminated = &terminated;
 	js_callback.auto_terminate = TRUE;
+	js_callback.events_supported = TRUE;
 
 	bool success=false;
 	bool rooted=false;
@@ -1887,16 +1888,9 @@ void input_thread(void *arg)
 	while(sbbs->online && sbbs->client_socket!=INVALID_SOCKET
 		&& node_socket[sbbs->cfg.node_num-1]!=INVALID_SOCKET) {
 
-		if(pthread_mutex_lock(&sbbs->input_thread_mutex)!=0)
-			sbbs->errormsg(WHERE,ERR_LOCK,"input_thread_mutex",0);
-
 #ifdef _WIN32	// No spy sockets
-		if (!socket_readable(sbbs->client_socket, 1000)) {
-			if(pthread_mutex_unlock(&sbbs->input_thread_mutex)!=0)
-				sbbs->errormsg(WHERE,ERR_UNLOCK,"input_thread_mutex",0);
-			YIELD();	/* This kludge is necessary on some Linux distros */
-			continue;	/* to allow other threads to lock the input_thread_mutex */
-		}
+		if (!socket_readable(sbbs->client_socket, 1000))
+			continue;
 #else
 #ifdef PREFER_POLL
 		fds[0].fd = sbbs->client_socket;
@@ -1910,22 +1904,15 @@ void input_thread(void *arg)
 			nfds++;
 		}
 
-		if (poll(fds, nfds, 1000) < 1) {
-			if(pthread_mutex_unlock(&sbbs->input_thread_mutex)!=0)
-				sbbs->errormsg(WHERE,ERR_UNLOCK,"input_thread_mutex",0);
-			YIELD();	/* This kludge is necessary on some Linux distros */
-			continue;	/* to allow other threads to lock the input_thread_mutex */
-		}
+		if (poll(fds, nfds, 1000) < 1)
+			continue;
 #else
 #error Spy sockets without poll() was removed in commit 3971ef4dcc3db19f400a648b6110718e56a64cf3
 #endif
 #endif
 
-		if(sbbs->client_socket==INVALID_SOCKET) {
-			if(pthread_mutex_unlock(&sbbs->input_thread_mutex)!=0)
-				sbbs->errormsg(WHERE,ERR_UNLOCK,"input_thread_mutex",0);
+		if(sbbs->client_socket==INVALID_SOCKET)
 			break;
-		}
 
 /*         ^          ^
  *      \______    ______/
@@ -1949,22 +1936,17 @@ void input_thread(void *arg)
 				close_socket(uspy_socket[sbbs->cfg.node_num-1]);
 				lprintf(LOG_NOTICE,"Closing local spy socket: %d",uspy_socket[sbbs->cfg.node_num-1]);
 				uspy_socket[sbbs->cfg.node_num-1]=INVALID_SOCKET;
-				if(pthread_mutex_unlock(&sbbs->input_thread_mutex)!=0)
-					sbbs->errormsg(WHERE,ERR_UNLOCK,"input_thread_mutex",0);
 				continue;
 			}
 			sock=uspy_socket[sbbs->cfg.node_num-1];
 		}
 		else {
-			if(pthread_mutex_unlock(&sbbs->input_thread_mutex)!=0)
-				sbbs->errormsg(WHERE,ERR_UNLOCK,"input_thread_mutex",0);
 			continue;
 		}
 #else
 #error Spy sockets without poll() was removed in commit 3971ef4dcc3db19f400a648b6110718e56a64cf3
 #endif
 #endif
-
     	rd=RingBufFree(&sbbs->inbuf);
 
 		if(rd==0) { // input buffer full
@@ -1974,16 +1956,16 @@ void input_thread(void *arg)
             while((rd=RingBufFree(&sbbs->inbuf))==0 && time(NULL)-start<5) {
                 YIELD();
             }
-			if(rd==0) {	/* input buffer still full */
-				if(pthread_mutex_unlock(&sbbs->input_thread_mutex)!=0)
-					sbbs->errormsg(WHERE,ERR_UNLOCK,"input_thread_mutex",0);
+			if(rd==0)	/* input buffer still full */
 				continue;
-			}
 		}
 
 	    if(rd > (int)sizeof(inbuf))
         	rd=sizeof(inbuf);
 
+		if(pthread_mutex_lock(&sbbs->input_thread_mutex)!=0)
+			sbbs->errormsg(WHERE,ERR_LOCK,"input_thread_mutex",0);
+
 #ifdef USE_CRYPTLIB
 		if(sbbs->ssh_mode && sock==sbbs->client_socket) {
 			int err;
@@ -2015,6 +1997,9 @@ void input_thread(void *arg)
 		if(pthread_mutex_unlock(&sbbs->input_thread_mutex)!=0)
 			sbbs->errormsg(WHERE,ERR_UNLOCK,"input_thread_mutex",0);
 
+		if (rd == 0 && !socket_recvdone(sock, 0))
+			continue;
+
 		if(rd == SOCKET_ERROR)
 		{
 #ifdef __unix__
@@ -2163,10 +2148,8 @@ void passthru_thread(void* arg)
 	thread_up(FALSE /* setuid */);
 
 	while(sbbs->online && sbbs->passthru_socket!=INVALID_SOCKET && !terminate_server) {
-		if (!socket_readable(sbbs->passthru_socket, 1000)) {
-			YIELD();	/* This kludge is necessary on some Linux distros */
-			continue;	/* to allow other threads to lock the input_thread_mutex */
-		}
+		if (!socket_readable(sbbs->passthru_socket, 1000))
+			continue;
 
 		if(sbbs->passthru_socket==INVALID_SOCKET)
 			break;
diff --git a/src/sbbs3/sbbs.h b/src/sbbs3/sbbs.h
index ff71c7fba647e17a3fa9be3b257ddc56a36a7178..5063311df440b88ee1cbef927dae590c99a139d2 100644
--- a/src/sbbs3/sbbs.h
+++ b/src/sbbs3/sbbs.h
@@ -297,6 +297,82 @@ extern int	thread_suid_broken;			/* NPTL is no longer broken */
 #include "getmail.h"
 #include "msg_id.h"
 
+#if defined(JAVASCRIPT)
+enum js_event_type {
+	JS_EVENT_SOCKET_READABLE_ONCE,
+	JS_EVENT_SOCKET_READABLE,
+	JS_EVENT_SOCKET_WRITABLE_ONCE,
+	JS_EVENT_SOCKET_WRITABLE,
+	JS_EVENT_SOCKET_CONNECT,
+	JS_EVENT_INTERVAL,
+	JS_EVENT_TIMEOUT,
+	JS_EVENT_CONSOLE_INPUT_ONCE,
+	JS_EVENT_CONSOLE_INPUT
+};
+
+struct js_event_interval {
+	uint64_t last;	// The tick the last event should have triggered at
+	uint64_t period;
+};
+
+struct js_event_timeout {
+	uint64_t end;	// Time the timeout expires
+};
+
+struct js_event_connect {
+	SOCKET sv[2];
+	SOCKET sock;
+};
+
+struct js_event_list {
+	struct js_event_list *prev;
+	struct js_event_list *next;
+	JSFunction *cb;
+	JSObject *cx;
+	union {
+		SOCKET	sock;
+		struct js_event_connect connect;
+		struct js_event_interval interval;
+		struct js_event_timeout timeout;
+	} data;
+	int32 id;
+	enum js_event_type type;
+};
+
+struct js_runq_entry {
+	JSFunction *func;
+	JSObject *cx;
+	struct js_runq_entry *next;
+};
+
+struct js_listener_entry {
+	char *name;
+	JSFunction *func;
+	int32 id;
+	struct js_listener_entry *next;
+};
+
+typedef struct js_callback {
+	struct js_event_list	*events;
+	struct js_runq_entry    *rq_head;
+	struct js_runq_entry    *rq_tail;
+	struct js_listener_entry *listeners;
+	volatile BOOL*	terminated;
+	struct js_callback	*parent_cb;
+	uint32_t		counter;
+	uint32_t		limit;
+	uint32_t		yield_interval;
+	uint32_t		gc_interval;
+	uint32_t		gc_attempts;
+	uint32_t		offline_counter;
+	int32			next_eid;
+	BOOL			auto_terminate;
+	BOOL			keepGoing;
+	BOOL			bg;
+	BOOL			events_supported;
+} js_callback_t;
+#endif
+
 /* Synchronet Node Instance class definition */
 #if defined(__cplusplus) && defined(JAVASCRIPT)
 
@@ -1305,6 +1381,8 @@ extern "C" {
 	DLLEXPORT void		DLLCALL js_EvalOnExit(JSContext*, JSObject*, js_callback_t*);
 	DLLEXPORT void		DLLCALL	js_PrepareToExecute(JSContext*, JSObject*, const char *filename, const char* startup_dir, JSObject *);
 	DLLEXPORT char*		DLLCALL js_getstring(JSContext *cx, JSString *str);
+	DLLEXPORT JSBool	js_handle_events(JSContext *cx, js_callback_t *cb, volatile int *terminated);
+	DLLEXPORT JSBool	js_clear_event(JSContext *cx, uintN argc, jsval *arglist, enum js_event_type et);
 
 	/* js_system.c */
 	DLLEXPORT JSObject* DLLCALL js_CreateSystemObject(JSContext* cx, JSObject* parent
@@ -1379,6 +1457,8 @@ extern "C" {
 
 	/* js_console.cpp */
 	JSObject* js_CreateConsoleObject(JSContext* cx, JSObject* parent);
+	DLLEXPORT size_t js_cx_input_pending(JSContext *cx);
+	DLLEXPORT void js_do_lock_input(JSContext *cx, JSBool lock);
 
 	/* js_bbs.cpp */
 	JSObject* js_CreateBbsObject(JSContext* cx, JSObject* parent);
diff --git a/src/sbbs3/sbbsdefs.h b/src/sbbs3/sbbsdefs.h
index cf2d6bf5f9d752d338a463d479e3bd4e7dc9cf17..be1fb144f60b3b429f192ae07b45b7d8ae25ffb3 100644
--- a/src/sbbs3/sbbsdefs.h
+++ b/src/sbbs3/sbbsdefs.h
@@ -68,20 +68,6 @@
 #define JAVASCRIPT_LOAD_PATH_LIST	"load_path_list"
 #define JAVASCRIPT_OPTIONS			0x810	// JSOPTION_JIT | JSOPTION_COMPILE_N_GO
 
-struct js_callback;
-typedef struct js_callback {
-	uint32_t		counter;
-	uint32_t		limit;
-	uint32_t		yield_interval;
-	uint32_t		gc_interval;
-	uint32_t		gc_attempts;
-	uint32_t		offline_counter;
-	BOOL			auto_terminate;
-	volatile BOOL*	terminated;
-	BOOL			bg;
-	struct js_callback	*parent_cb;
-} js_callback_t;
-
 #define JSVAL_NULL_OR_VOID(val)		(JSVAL_IS_NULL(val) || JSVAL_IS_VOID(val))
 
 #ifndef MAX
diff --git a/src/sbbs3/services.c b/src/sbbs3/services.c
index a27eb58aa8aed63a7201ce502681ecf3c0e142c2..11efc7f59b38a1862f4eb4158edb2d080a1d14cf 100644
--- a/src/sbbs3/services.c
+++ b/src/sbbs3/services.c
@@ -1147,9 +1147,11 @@ static void js_service_thread(void* arg)
 		if(service->log_level >= LOG_ERR)
 			lprintf(LOG_ERR,"%04d !JavaScript FAILED to compile script (%s)",socket,spath);
 	} else {
+		service_client.callback.events_supported = TRUE;
 		js_PrepareToExecute(js_cx, js_glob, spath, /* startup_dir */NULL, js_glob);
 		JS_SetOperationCallback(js_cx, js_OperationCallback);
 		JS_ExecuteScript(js_cx, js_glob, js_script, &rval);
+		js_handle_events(js_cx, &service_client.callback, &terminated);
 		js_EvalOnExit(js_cx, js_glob, &service_client.callback);
 	}
 	JS_RemoveObjectRoot(js_cx, &js_glob);
@@ -1259,8 +1261,10 @@ static void js_static_service_thread(void* arg)
 			break;
 		}
 
+		service_client.callback.events_supported = TRUE;
 		js_PrepareToExecute(js_cx, js_glob, spath, /* startup_dir */NULL, js_glob);
 		JS_ExecuteScript(js_cx, js_glob, js_script, &rval);
+		js_handle_events(js_cx, &service_client.callback, &terminated);
 		js_EvalOnExit(js_cx, js_glob, &service_client.callback);
 		JS_RemoveObjectRoot(js_cx, &js_glob);
 		JS_ENDREQUEST(js_cx);
diff --git a/src/sbbs3/websrvr.c b/src/sbbs3/websrvr.c
index f6edf8df5333b50eb1911ffa511b59837a452f74..bc52448bc9999f601469085e3eb3da3cf98d298c 100644
--- a/src/sbbs3/websrvr.c
+++ b/src/sbbs3/websrvr.c
@@ -7157,6 +7157,7 @@ void DLLCALL web_server(void* arg)
 			session->js_callback.limit=startup->js.time_limit;
 			session->js_callback.gc_interval=startup->js.gc_interval;
 			session->js_callback.yield_interval=startup->js.yield_interval;
+			session->js_callback.events_supported = FALSE;
 			pthread_mutex_unlock(&session->struct_filled);
 			session=NULL;
 			served++;
diff --git a/src/xpdev/sockwrap.c b/src/xpdev/sockwrap.c
index eee5ed44398d83493c5256efd34bf8f70ba601e3..b9c5eb5b40a1449a7fb27e21a4b3a8cf2a9d5b93 100644
--- a/src/xpdev/sockwrap.c
+++ b/src/xpdev/sockwrap.c
@@ -733,3 +733,81 @@ DLLEXPORT int xp_inet_pton(int af, const char *src, void *dst)
 	freeaddrinfo(res);
 	return 1;
 }
+
+#ifdef _WIN32
+DLLEXPORT int
+socketpair(int domain, int type, int protocol, SOCKET *sv)
+{
+	union xp_sockaddr la = {0};
+	const int ra = 1;
+	SOCKET ls;
+	SOCKET *check;
+	fd_set rfd;
+	struct timeval tv;
+	size_t sa_len;
+
+	sv[0] = sv[1] = INVALID_SOCKET;
+	ls = socket(domain, type, protocol);
+	if (ls == INVALID_SOCKET)
+		goto fail;
+	switch (domain) {
+		case PF_INET:
+			if (inet_ptoaddr("127.0.0.1", &la, sizeof(la)) == NULL)
+				goto fail;
+			sa_len = sizeof(la.in);
+			break;
+		case PF_INET6:
+			if (inet_ptoaddr("::1", &la, sizeof(la)) == NULL)
+				goto fail;
+			sa_len = sizeof(la.in6);
+			break;
+		default:
+			goto fail;
+	}
+	inet_setaddrport(&la, 0);
+	if (setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, (const char *)&ra, sizeof(ra)) == -1)
+		goto fail;
+	if (bind(ls, &la.addr, sa_len) == -1)
+		goto fail;
+	if (getsockname(ls, &la.addr, &sa_len) == -1)
+		goto fail;
+	if (listen(ls, 1) == -1)
+		goto fail;
+	sv[0] = socket(la.addr.sa_family, type, protocol);
+	if (sv[0] == INVALID_SOCKET)
+		goto fail;
+	if (connect(sv[0], &la.addr, sa_len) == -1)
+		goto fail;
+	sv[1] = accept(ls, NULL, NULL);
+	if (sv[1] == INVALID_SOCKET)
+		goto fail;
+	closesocket(ls);
+	ls = INVALID_SOCKET;
+
+	if (send(sv[1], (const char *)&sv, sizeof(sv), 0) != sizeof(sv))
+		goto fail;
+	tv.tv_sec = 0;
+	tv.tv_usec = 50000;
+	FD_ZERO(&rfd);
+	FD_SET(sv[0], &rfd);
+	if (select(sv[0] + 1, &rfd, NULL, NULL, &tv) != 1)
+		goto fail;
+	if (recv(sv[0], (char *)&check, sizeof(check), 0) != sizeof(check))
+		goto fail;
+	if (check != sv)
+		goto fail;
+	return 0;
+
+fail:
+	if (ls != INVALID_SOCKET)
+		closesocket(ls);
+	if (sv[0] != INVALID_SOCKET)
+		closesocket(sv[0]);
+	sv[0] = INVALID_SOCKET;
+	if (sv[1] != INVALID_SOCKET)
+		closesocket(sv[1]);
+	sv[1] = INVALID_SOCKET;
+	return -1;
+}
+#endif
+
diff --git a/src/xpdev/sockwrap.h b/src/xpdev/sockwrap.h
index 79f394669312d72e57e184a14894df07bfdf4d3f..64481c2f5e74ad28b14d0aac2b222c0231ea0bd6 100644
--- a/src/xpdev/sockwrap.h
+++ b/src/xpdev/sockwrap.h
@@ -230,6 +230,7 @@ DLLEXPORT void set_socket_errno(int);
 DLLEXPORT int xp_inet_pton(int af, const char *src, void *dst);
 #if defined(_WIN32) // mingw and WinXP's WS2_32.DLL don't have inet_pton():
 	#define inet_pton	xp_inet_pton
+DLLEXPORT int socketpair(int domain, int type, int protocol, SOCKET *sv);
 #endif
 
 /*