var Reflect = require("./reflect.js"); var Minimart = require("./minimart.js"); var World = Minimart.World; var Route = Minimart.Route; Actor._chunks = null; function Actor(ctor) { var oldChunks = Actor._chunks; try { Actor._chunks = []; var behavior = new ctor(); return finalizeActor(behavior, Actor._chunks); } 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, 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 oldBoot = behavior.boot; var oldHandleEvent = behavior.handleEvent; var projections = {}; var compiledProjections = {}; var previousObjs = {}; behavior.boot = function () { if (oldBoot) { oldBoot.call(this); } for (var i = 0; i < chunks.length; i++) { var chunk = chunks[i]; if (chunk.kind === 'observer') { if (chunk.options.presence) { this[chunk.options.presence] = false; } if (chunk.options.name) { this[chunk.options.name] = []; } if (chunk.options.added) { this[chunk.options.added] = []; } if (chunk.options.removed) { this[chunk.options.removed] = []; } } } this.updateRoutes(); }; 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.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.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); } } }; return behavior; } 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;