forked from airbnb/hypernova
-
Notifications
You must be signed in to change notification settings - Fork 0
/
BatchManager.js
212 lines (185 loc) · 5.87 KB
/
BatchManager.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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
const noHTMLError = new TypeError(
'HTML was not returned to Hypernova, this is most likely an error within your application. ' +
'Check your logs for any uncaught errors and/or rejections.',
);
noHTMLError.stack = null;
function errorToSerializable(error) {
// istanbul ignore next
if (error === undefined) throw new TypeError('No error was passed');
// make sure it is an object that is Error-like so we can serialize it properly
// if it's not an actual error then we won't create an Error so that there is no stack trace
// because no stack trace is better than a stack trace that is generated here.
const err = (
Object.prototype.toString.call(error) === '[object Error]' &&
typeof error.stack === 'string'
) ? error : { name: 'Error', type: 'Error', message: error, stack: '' };
return {
type: err.type,
name: err.name,
message: err.message,
stack: err.stack.split('\n '),
};
}
function notFound(name) {
const error = new ReferenceError(`Component "${name}" not registered`);
const stack = error.stack.split('\n');
error.stack = [stack[0]]
.concat(
` at YOUR-COMPONENT-DID-NOT-REGISTER_${name}:1:1`,
stack.slice(1),
)
.join('\n');
return error;
}
function msSince(start) {
const diff = process.hrtime(start);
return (diff[0] * 1e3) + (diff[1] / 1e6);
}
function now() {
return process.hrtime();
}
/**
* The BatchManager is a class that is instantiated once per batch, and holds a lot of the
* key data needed throughout the life of the request. This ends up cleaning up some of the
* management needed for plugin lifecycle, and the handling of rendering multiple jobs in a
* batch.
*
* @param {express.Request} req
* @param {express.Response} res
* @param {Object} jobs - a map of token => Job
* @param {Object} config
* @constructor
*/
class BatchManager {
constructor(request, response, jobs, config) {
const tokens = Object.keys(jobs);
this.config = config;
this.plugins = config.plugins;
this.error = null;
this.statusCode = 200;
// An object that all of the contexts will inherit from... one per instance.
this.baseContext = {
request,
response,
batchMeta: {},
};
// An object that will be passed into the context for batch-level methods, but not for job-level
// methods.
this.batchContext = {
tokens,
jobs,
};
// A map of token => JobContext, where JobContext is an object of data that is per-job,
// and will be passed into plugins and used for the final result.
this.jobContexts = tokens.reduce((obj, token) => {
const { name, data, metadata } = jobs[token];
/* eslint no-param-reassign: 1 */
obj[token] = {
name,
token,
props: data,
metadata,
statusCode: 200,
duration: null,
html: null,
returnMeta: {},
resLocals: response.locals,
};
return obj;
}, {});
// Each plugin receives it's own little key-value data store that is scoped privately
// to the plugin for the life time of the request. This is achieved simply through lexical
// closure.
this.pluginContexts = new Map();
this.plugins.forEach((plugin) => {
this.pluginContexts.set(plugin, { data: new Map() });
});
}
/**
* Returns a context object scoped to a specific plugin and job (based on the plugin and
* job token passed in).
*/
getRequestContext(plugin, token) {
return {
...this.baseContext,
...this.jobContexts[token],
...this.pluginContexts.get(plugin),
};
}
/**
* Returns a context object scoped to a specific plugin and batch.
*/
getBatchContext(plugin) {
return {
...this.baseContext,
...this.batchContext,
...this.pluginContexts.get(plugin),
};
}
contextFor(plugin, token) {
return token ? this.getRequestContext(plugin, token) : this.getBatchContext(plugin);
}
/**
* Renders a specific job (from a job token). The end result is applied to the corresponding
* job context. Additionally, duration is calculated.
*/
render(token) {
const start = now();
const context = this.jobContexts[token];
const { name } = context;
const { getComponent } = this.config;
const result = getComponent(name, context);
return Promise.resolve(result).then((renderFn) => {
// ensure that we have this component registered
if (!renderFn || typeof renderFn !== 'function') {
// component not registered
context.statusCode = 404;
return Promise.reject(notFound(name));
}
return renderFn(context.props);
}).then((html) => { // eslint-disable-line consistent-return
if (!html) {
return Promise.reject(noHTMLError);
}
context.html = html;
context.duration = msSince(start);
}).catch((err) => {
context.duration = msSince(start);
return Promise.reject(err);
});
}
recordError(error, token) {
if (token && this.jobContexts[token]) {
const context = this.jobContexts[token];
context.statusCode = context.statusCode === 200 ? 500 : context.statusCode;
context.error = error;
} else {
this.error = error;
this.statusCode = 500;
}
}
getResult(token) {
const context = this.jobContexts[token];
return {
name: context.name,
html: context.html,
meta: context.returnMeta,
duration: context.duration,
statusCode: context.statusCode,
success: context.html !== null,
error: context.error ? errorToSerializable(context.error) : null,
};
}
getResults() {
return {
success: this.error === null,
error: this.error,
results: Object.keys(this.jobContexts).reduce((result, token) => {
/* eslint no-param-reassign: 1 */
result[token] = this.getResult(token);
return result;
}, {}),
};
}
}
export default BatchManager;