-
Notifications
You must be signed in to change notification settings - Fork 5
/
LogicSaga.js
171 lines (156 loc) · 6.15 KB
/
LogicSaga.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
/**
* @module esdf/core/LogicSaga
*/
var EventSourcedAggregate = require('./EventSourcedAggregate.js').EventSourcedAggregate;
var Event = require('./Event.js').Event;
var util = require('util');
var when = require('when');
var whenFunctions = require('when/function');
function LogicSagaTimerIDConflictError(id){
this.name = 'LogicSagaTimerIDConflictError';
this.message = 'This timer ID (' + id + ') has already been used by this saga - it may not be re-used.';
this.data = {
id: id
};
}
util.inherits(LogicSagaTimerIDConflictError, Error);
function LogicSagaTimerHandlerMissingError(timerID, timerType){
this.name = 'LogicSagaTimerHandlerMissingError';
this.message = 'There is no handler defined for this type of timer (' + timerType + ') - bailing out.';
this.data = {
timerID: timerID,
timerType: timerType
};
}
//TODO: LogicSaga API documentation & overview.
function LogicSaga(){
//TODO: Find a way to document the ensured members without duplicating code. Using JSDoc3 should also be possible in non-constructor functions...
this._ensureMembers();
/**
* Functions used to handle incoming events and direct transitions to further stages. Keyed by stage name.
* This is the de facto stage definition mechanism.
* @type {Object.<string,function>}
*/
this._stageHandlers = {};
/**
* Name of the current stage handler that should be used. Corresponds to a key from the _stageHandlers map.
* @type {string}
*/
this._currentStage = undefined;
/**
* A map of timers that have been activated by this saga. Time-dependent triggers use the timer IDs to signal that the time has elapsed.
* Keyed by timer ID, the values represent the timer types (defined by the saga implementation for its own use).
* The timer types correspond to timer trigger handlers.
* @type {Object.<string,string>}
*/
this._activeTimers = {};
/**
* A set of timer IDs that have been processed in the past. Does not include active timers.
*/
this._processedTimers = {};
}
util.inherits(LogicSaga, EventSourcedAggregate);
LogicSaga.prototype._aggregateType = 'LogicSaga';
LogicSaga.prototype._ensureMembers = function _ensureMembers(){
if(!this._seenEventIDs){
this._seenEventIDs = {};
}
if(!this._acceptedEvents){
this._acceptedEvents = [];
}
if(!this._activeTimers){
this._activeTimers = [];
}
if(!this._processedTimers){
this._processedTimers = {};
}
};
LogicSaga.prototype._switchStage = function _switchStage(newStageName){
this._currentStage = String(newStageName);
this._acceptedEvents = [];
};
LogicSaga.prototype._setupTimer = function _setupTimer(timerID, timerType, plannedTriggerTime, setupTime){
// Guard clause: check whether the timer is already active or has been used in the past.
if(this._activeTimers.indexOf(timerID) >= 0 || this._processedTimers[timerID]){
throw new LogicSagaTimerIDConflictError(timerID);
}
this._stageEvent(new Event('TimerSetup', {
timerID: timerID,
timerType: timerType,
setupTime: setupTime ? (new Date(setupTime)) : (new Date()), // Audit purposes only
plannedTriggerTime: new Date(plannedTriggerTime)
}));
};
//TODO: Let the user provide custom handling for timers by declaring this function abstract (at least in the doc...).
LogicSaga.prototype.handleTimer = function handleTimer(timerID, actualTriggerTime, deps){
var self = this;
// NOTE: Dynamic dispatch! Watch out for name collisions!
var timerType = this._activeTimers[timerID];
var timerHandlerName = 'handle' + timerType + 'Timer';
this._finishTimer(timerID, actualTriggerTime);
if(typeof(this[timerHandlerName]) === 'function'){
return whenFunctions.call(this[timerHandlerName].bind(this), timerID, actualTriggerTime, deps);
}
else{
throw new LogicSagaTimerHandlerMissingError(timerID, timerType);
}
};
LogicSaga.prototype._finishTimer = function _finishTimer(timerID, actualTriggerTime){
this._stageEvent(new Event('TimerFinished', {
timerID: String(timerID),
actualTriggerTime: new Date(actualTriggerTime)
}));
};
LogicSaga.prototype.onEventProcessed = function onEventProcessed(event, commit){
this._acceptedEvents.push(event.eventPayload.event);
this._seenEventIDs[event.eventPayload.event.eventID] = true;
};
LogicSaga.prototype.onTimerSetup = function onTimerSetup(event, commit){
this._activeTimers[event.eventPayload.timerID] = event.eventPayload.timerType;
};
LogicSaga.prototype.onTimerFinished = function onTimerFinished(event, commit){
var timerID = event.eventPayload.timerID;
this._activeTimers = this._activeTimers.filter(function _removeFinishedTimer(timerEntry){
return timerEntry !== timerID;
});
this._processedTimers[timerID] = true;
};
LogicSaga.prototype.processEvent = function processEvent(event, commit, environment){
var self = this;
this._ensureMembers();
// Guard clause: catch duplicate events and disregard them. The promise is resolved, so that the upper layers may release responsibility for the event, too.
if(this._seenEventIDs[event.eventID]){
return when.resolve();
}
// First, mark the event as processed:
self._stageEvent(new Event('EventProcessed', {
event: event,
commit: commit
}));
// Obtain the stage handler that we should use.
var currentHandler = this._stageHandlers[this._currentStage];
// Run the handler and get a promise that will eventually indicate the transition (or the lack of) to be performed.
var transitionPromise = currentHandler.call(this, event, commit, environment);
return when(transitionPromise).then(
function _transitionDecisionReached(transitionEvent){
// Next, act upon the transition decision:
if(typeof(transitionEvent) === 'object' && transitionEvent !== null && typeof(transitionEvent.eventType) === 'string' && transitionEvent.eventType.length > 0){
// There is a transition to be made. Carry it out.
self._stageEvent(transitionEvent);
}
else{
// No transition chosen at this time. Do nothing.
}
return when.resolve();
},
function _eventRejected(reason){
return when.reject(reason);
});
};
LogicSaga.getBinds = function getBinds(){
return [];
};
LogicSaga.route = function route(event, commit){
throw new Error('The routing function needs to be overloaded by child prototypes - refusing to act with the default routing function');
};
module.exports.LogicSaga = LogicSaga;