Niro* by cho45

#21 micro-template.js JavaScript Template Engine

Released first version of micro-template.js to npm: https://npmjs.org/micro-template (repository: http://github.com/cho45/micro-template.js ). Of Course, you can use this in HTML pages.

This is inspired from John Resig's JavaScript Micro-Templating. It is awesome because very leverage small code. But wrote from scratch as following reason:

  • Better error messages: show line-number in runtime errors
  • Support source map: debug is more easily on Chrome including syntax errors
  • Well tested: tested on node.js
  • Escape by default: all output is escaped by default for security

And, as a result, more faster:

micro-template.js: 2156.721782890007 counts/sec
John Resig's tmpl: 1371.7421124828534 counts/sec
micro-template.js (escaped): 1874.414245548266 counts/sec

Of Course, this is still very small code. 27 lines: https://github.com/cho45/micro-template.js/blob/master/lib/micro-template.js

I love leverage. Enjoy.

#20 Multiple values per key object for JavaScript. hash-multivalue.js

In some cases, I want an object which can have multiple values for a key, for example, a parsed object of query parameters or HTTP headers. Here is an implement of the object, hash-multivalue.js.

npm install hash-multivalue
var hash = HashMultiValue({ foo : ['aaa', 'bbb'], bar : ['111', '222', '333'] });
hash; //=> { foo: 'bbb', bar: '333' }
hash.foo //=> 'bbb'
hash.get('foo') //=> 'bbb'
hash.getAll('foo') //=> ['aaa', 'bbb']

hash-multivalue.js is very inspired from Perl's Hash::MultiValue. So I need specs as following:

  • The object is like plain object. In other words, a value for a key must be accessed by property access syntax.

So I use a bit trick to realizing to do it.

  • An instance is normal key / value which is last value of the array of a key.
    • This is able to access a value by property access syntax.
  • An instance has each prototype for it.
    • The instance methods are implemented on it. The prototype object must be instance-specific, so this creates constructor function for each instance.
  • Use closure to retain original key / values (array) pair.
    • Properties are as less as possible.

JavaScript does not distinguish a method or a propety, so the names of methods (eg. 'get', 'getAll') cannot be accessed by property syntax (Use the methods instead). This is restriction of language.

#19 console.log in demo code

Codes in JSDeferred Cookbook is evaluated in browser as is. But I want output of console.log to show in general. So I had to override console.log and do it with following solution:

Codes are all string so there are two way to evaluate it as JS: eval() or new Function(code)(). In this case, I use latter because I want to override console.log.

var code = ...;
new Function('console', code)({
  log : function (arg) {
    ...
  }
});

I created a dummy console object which has log function and passed it into functioned demo code with arguments named console. And then, all of console.log in code are belong to us.

This way has very high advantage for asynchronous code. If I use the way overriding original console.log and write back after doing, I must handling all asynchronous event.

#18 JSDeferred in Jetpack (Add-on SDK). Easy to dialogues between chrome and content.

I wrote a binding of JSDeferred*1 to Jetpack (Add-on SDK). This makes very easy to message dialogues between chrome and content in some cases.

Load this library and you can use Deferred.postie(constructor, options) function for creating new widget or panel.

A instance of a widget or panel created by Deferred.postie has post() method and bind() method.

  • post(args..., function) //=> Deferred
    • Call function with args in the content context of receiver and get the result as Deferred.
  • bind(selector, event, function) //=> Deferred
    • Bind event to all matched elements by selector in content context and call function in chrome context as a event is fired.

So you can write basic message dialogue with following:

const Deferred = require("jsdeferred").Deferred;
Deferred.define(this);

widget = Deferred.postie(widgets.Widget, {
	label      : "Foo",
	contentURL : 'http://example.com/',
	width      : 32,
	onClick    : function () {
		var self = this;

		next(function () {
			return self.
				post(1, 2, function (a, b) { // content context 
					return a + b;
				}).
				next(function (res) { // chrome context
					console.log(res);
				});
		}).
		next(function () {
			return wait(1);
		}).
		next(function () {
			return self.
				post(function (a, b) {
					throw "foo";
				}).
				next(function (res) {
					console.log(res);
				}).
				error(function (e) {
					console.log(e);
				});
		});
	},
	onMessage : function (message) {
		console.log(message);
	},
	contentScript : 'setTimeout(function () { postMessage("custom message") }, 1000)',
});

widget.post(function () {
	var d = new Deferred();
	setTimeout(function () {
		d.call(1);
	}, 1000);
	return d;
}).
next(function (a) {
	console.log("Hello! " + a);
}).
error(function (e) {
	console.log(e);
});

widget.bind("body", "click", function (e) {
	console.log("body clicked" + e);
}).
error(function (e) {
	console.log(e);
});

[[javascript]] [[jsdeferred]] [[jetpack]]

#17 pub/sub message queue with HTTP, simple implementation on MongoDB

  • Using MongoDB as a storage of callback list and buffer
  • Using HTTP as push message
  1. First, a subscriber requests to /sub with key which is wanted.
  2. When a publisher fired a event about a key, it requests to /pub with the key with a message
  3. This message queue server receives a pub request and turns over the message to subscribers by HTTP.

This is only for internal usage of web application. There are no authentications.

package Buspub;

use strict;
use warnings;
sub route ($$);
use Router::Simple;
use Tie::IxHash;
use AnyEvent::HTTP;
use HTTP::Request::Common;
use JSON::XS;
use POSIX ();

use MongoDB;
my $connection = MongoDB::Connection->new(host => 'localhost', port => 27017);
my $subscriptions = $connection->get_database('pubsub')->subscriptions;
$subscriptions->ensure_index(Tie::IxHash->new( key => 1, created => -1 ), { background => 1 });

route '/sub' => {
	action => sub {
		my ($r) = @_;
		my $key = $r->req->param('key') or return $r->json({ error => "key required"});;
		my $callback = $r->req->param('callback') or return $r->json({ error => "callback required"});;
		my $id = $subscriptions->insert({
			key      => $key,
			messages => [],
			callback => $callback,
			max      => 25,
			created  => scalar time(),
		});
		LOG("SUB:: %s with callback: %s", $key, $callback);
		$r->json({ id => "$id" });
	}
};

route '/pub' => {
	action => sub {
		my ($r) = @_;
		my $key = $r->req->param('key') or return $r->json({ error => "key required"});;
		my $message = decode_json $r->req->param('message');

		my $cursor = $subscriptions->query({ key => $key });
		my %count;
		while (my $obj = $cursor->next) {
			$count{all}++;
			my $id  = $obj->{_id} . "";
			my $uri = $obj->{callback};
			my $messages = [ @{$obj->{messages}}, $message ];
			my $req = POST $uri, [ id => $id, messages => encode_json($messages) ];

			LOG("PUB:: => %s => %s with %d messages", $key, $req->uri, scalar @$messages);
			http_request $req->method => $req->uri,
				body => $req->content,
				headers => {
					map { $_ => $req->header($_), } $req->headers->header_field_names
				},
				timeout => 20,
				sub {
					my ($body, $headers) = @_;
					LOG("PUB:: <= %s <= %s with status:%d", $key, $req->uri, $headers->{Status});
					if ($headers->{Status} =~ /^2/) {
						$subscriptions->update({ _id => $obj->{_id} }, { '$pullAll' => { messages => $obj->{messages} } });
					} elsif ($headers->{Status} =~ /^4/) {
						$subscriptions->remove({ _id => $obj->{_id} });
					} elsif ($headers->{Status} =~ /^5/) {
						if (@$messages > $obj->{max}) {
							$subscriptions->remove({ _id => $obj->{_id} });
						} else {
							$subscriptions->update({ _id => $obj->{_id} }, { '$push' => { messages => $message } });
						}
					}
				}
			;
		}
		$r->json({ key => $key, delivered => \%count });
	}
};

route '/test/callback' =>  {
	action => sub {
		my ($r) = @_;
		use Data::Dumper;
		warn Dumper $r->req->param('id') ;
		warn Dumper decode_json $r->req->param('messages') ;
		$r->res->status($r->req->param('code') || 404);
		$r->res->content_type('application/json; charset=utf8');
		$r->res->content('{}');
		$r->res->finalize;
	}
};


BEGIN {
	my $router = Router::Simple->new;
	sub route ($$) { $router->connect(@_) };

	sub run {
		my ($env) = @_;
		if ( my $handler = $router->match($env) ) {
			my $c = Buspub::Context->new($env);
			$handler->{action}->($c);
		} else {
			[ 404, [ 'Content-Type' => 'text/html' ], ['Not Found'] ];
		}
	}

	sub LOG {
		my ($message, @args) = @_;
		print sprintf("[%s] $message", POSIX::strftime("%Y-%m-%d %H:%M:%S", localtime), @args), "\n";
	}
};

package Buspub::Request;
use parent qw(Plack::Request);

package Buspub::Response;
use parent qw(Plack::Response);

package Buspub::Context;
use JSON::XS;

sub new {
	my ($class, $env) = @_;
	bless {
		req => Buspub::Request->new($env),
		res => Buspub::Response->new(200),
	}, $class;
}

sub req { $_[0]->{req} }
sub res { $_[0]->{res} }

sub json {
	my ($self, $vars) = @_;
	my $body = JSON::XS->new->ascii(1)->encode($vars);
	$self->res->content_type('application/json; charset=utf8');
	$self->res->content($body);
	$self->res->finalize;
}

\&Buspub::run;

[[perl]] [[http]] [[webhook]] [[messagequeue]]

login