277 lines
7.6 KiB
JavaScript
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;
|