js-marketplace-2014/src/actor.js

277 lines
7.6 KiB
JavaScript

var Reflect = require("./reflect.js");
var Minimart = require("./minimart.js");
var World = Minimart.World;
var Route = Minimart.Route;
Actor._chunks = null;
function Actor(bootfn) {
return {
boot: function () {
delete this.boot;
var oldChunks = Actor._chunks;
try {
Actor._chunks = [];
bootfn.call(this);
finalizeActor(this, Actor._chunks);
Actor._chunks = oldChunks;
} catch (e) {
Actor._chunks = oldChunks;
throw e;
}
}
};
}
function checkChunks(type) {
if (!Actor._chunks) {
throw new Error("Call to Actor."+type+" outside of Actor constructor");
}
}
function extractChunk(type, kind, defaultOptions, args) {
var rawProjectionFn = args[0]
var options = null;
var handler = null;
if (typeof rawProjectionFn !== 'function') {
throw new Error("Actor."+type+" expects a function producing a pattern as first argument");
}
for (var i = 1; i < args.length; i++) { // NB: skip the first arg - it's rawProjectionFn
if (typeof args[i] === 'function') {
if (handler !== null) { throw new Error("Too many handler functions in Actor."+type); }
handler = args[i];
} else if (typeof args[i] === 'object') {
if (options !== null) { throw new Error("Too many options arguments in Actor."+type); }
options = args[i];
} else {
throw new Error("Unrecognised argument in Actor."+type);
}
}
options = options || {};
for (var k in options) {
if (!(k in defaultOptions)) {
throw new Error("Unrecognised option '"+k+"' in Actor."+type);
}
}
for (var k in defaultOptions) {
if (!(k in options)) {
options[k] = defaultOptions[k];
}
}
return {
type: type,
kind: kind,
rawProjectionFn: rawProjectionFn,
options: options,
handler: handler
};
}
function recordChunk(chunk) {
Actor._chunks.push(chunk);
}
function chunkExtractor(type, kind, defaultOptions) {
return function (/* ... */) {
checkChunks(type);
recordChunk(extractChunk(type,
kind,
defaultOptions,
Array.prototype.slice.call(arguments)));
};
}
var participantDefaults = {
metaLevel: 0,
when: function () { return true; }
};
var observerDefaults = {
metaLevel: 0,
level: 0,
when: function () { return true; },
presence: null,
name: null,
singleton: null,
set: null,
added: null,
removed: null
};
Actor.advertise = chunkExtractor('advertise', 'participant', participantDefaults);
Actor.subscribe = chunkExtractor('subscribe', 'participant', participantDefaults);
Actor.observeAdvertisers = chunkExtractor('observeAdvertisers', 'observer', observerDefaults);
Actor.observeSubscribers = chunkExtractor('observeSubscribers', 'observer', observerDefaults);
Actor.observeGestalt = function (gestaltFn, eventHandlerFn) {
checkChunks('observeGestalt');
recordChunk({
type: 'observeGestalt',
kind: 'raw',
gestaltFn: gestaltFn,
options: {
when: function () { return true; }
},
eventHandlerFn: eventHandlerFn
});
};
function finalizeActor(behavior, chunks) {
var oldHandleEvent = behavior.handleEvent;
var projections = {};
var compiledProjections = {};
var previousObjs = {};
behavior.updateRoutes = function () {
var newRoutes = Route.emptyGestalt;
for (var i = 0; i < chunks.length; i++) {
var chunk = chunks[i];
if (chunk.options.when.call(this)) {
switch (chunk.kind) {
case 'raw':
newRoutes = newRoutes.union(chunk.gestaltFn.call(this));
break;
case 'participant':
var proj = chunk.rawProjectionFn.call(this);
projections[i] = proj;
var g = Route.simpleGestalt(chunk.type === 'advertise',
Route.projectionToPattern(proj),
chunk.options.metaLevel,
0);
newRoutes = newRoutes.union(g);
break;
case 'observer':
var proj = chunk.rawProjectionFn.call(this);
projections[i] = proj;
compiledProjections[i] = Route.compileProjection(proj);
var g = Route.simpleGestalt(chunk.type === 'observeSubscribers',
Route.projectionToPattern(proj),
chunk.options.metaLevel,
chunk.options.level + 1);
newRoutes = newRoutes.union(g);
if (chunk.options.added || chunk.options.removed) {
previousObjs[i] = Route.arrayToSet([]);
}
break;
default:
throw new Error("Unsupported chunk type/kind: "+chunk.type+"/"+chunk.kind);
}
}
}
World.updateRoutes([newRoutes]);
};
behavior.handleEvent = function (e) {
if (oldHandleEvent) { oldHandleEvent.call(this, e); }
for (var i = 0; i < chunks.length; i++) {
var chunk = chunks[i];
switch (chunk.kind) {
case 'raw':
chunk.eventHandlerFn.call(this, e);
break;
case 'participant':
if (chunk.handler
&& (e.type === 'message')
&& (e.metaLevel === chunk.options.metaLevel)
&& (e.isFeedback === (chunk.type === 'advertise')))
{
var matchResult = Route.matchPattern(e.message, projections[i]);
if (matchResult) {
kwApply(chunk.handler, this, matchResult);
}
}
break;
case 'observer':
if (e.type === 'routes') {
var projectionResult = e.gestalt.project(compiledProjections[i],
chunk.type !== 'observeSubscribers',
chunk.options.metaLevel,
chunk.options.level);
var isPresent = !Route.is_emptyMatcher(projectionResult);
if (chunk.options.presence) {
this[chunk.options.presence] = isPresent;
}
var objs = [];
if (isPresent) {
var keys = Route.matcherKeys(projectionResult);
if (keys === null) {
console.warn("Wildcard detected while projecting ("
+JSON.stringify(chunk.options)+")");
} else {
objs = Route.matcherKeysToObjects(keys, compiledProjections[i]);
if (chunk.options.set) {
for (var j = 0; j < objs.length; j++) {
objs[j] = chunk.options.set.call(this, objs[j]);
}
}
}
}
if (chunk.options.name) {
this[chunk.options.name] = objs;
}
if (chunk.options.singleton) {
this[chunk.options.singleton] = objs.length === 1 ? objs[0] : undefined;
}
if (chunk.options.added || chunk.options.removed) {
var objSet = Route.arrayToSet(objs);
if (chunk.options.added) {
this[chunk.options.added] =
Route.setToArray(Route.setSubtract(objSet, previousObjs[i]));
}
if (chunk.options.removed) {
this[chunk.options.removed] =
Route.setToArray(Route.setSubtract(previousObjs[i], objSet));
}
previousObjs[i] = objSet;
}
if (chunk.handler) {
chunk.handler.call(this);
}
}
break;
default:
throw new Error("Unsupported chunk type/kind: "+chunk.type+"/"+chunk.kind);
}
}
};
if (behavior.boot) { behavior.boot(); }
for (var i = 0; i < chunks.length; i++) {
var chunk = chunks[i];
if (chunk.kind === 'observer') {
if (chunk.options.presence) { behavior[chunk.options.presence] = false; }
if (chunk.options.name) { behavior[chunk.options.name] = []; }
if (chunk.options.singleton) { behavior[chunk.options.singleton] = undefined; }
if (chunk.options.added) { behavior[chunk.options.added] = []; }
if (chunk.options.removed) { behavior[chunk.options.removed] = []; }
}
}
behavior.updateRoutes();
}
function kwApply(f, thisArg, args) {
var formals = Reflect.formalParameters(f);
var actuals = []
for (var i = 0; i < formals.length; i++) {
var formal = formals[i];
if (!(formal in args)) {
throw new Error("Function parameter '"+formal+"' not present in args");
}
actuals.push(args[formal]);
}
return f.apply(thisArg, actuals);
}
///////////////////////////////////////////////////////////////////////////
module.exports.Actor = Actor;
module.exports.kwApply = kwApply;