UNPKG

14.8 kBPlain TextView Raw
1import { Request as BaseRequest, WorkerRouter as Router, Callback, CallbackResponse, workerErrorHandler, workerHandler, workerRequestHandler } from './router';
2import * as akala from '@akala/core';
3import * as express from 'express';
4import * as stream from 'stream';
5export { Router, Callback, workerHandler as RouterHandler, workerErrorHandler as ErrorHandler, workerRequestHandler as RequestHandler };
6import * as jsonrpc from '@akala/json-rpc-ws'
7import send from 'send';
8import onFinished from 'on-finished';
9const log = akala.log('akala:worker');
10
11export { CallbackResponse }
12
13export function createClient(namespace: string): PromiseLike<jsonrpc.ws.Client>
14{
15 return akala.resolve('$agent.' + namespace);
16}
17
18export type MasterRegistration = (from?: string, masterPath?: string, workerPath?: string) => void;
19export type IsModule = (moduleName: string) => boolean;
20
21export interface resolve
22{
23 (param: '$http'): akala.Http
24 (param: '$request'): Request
25 (param: '$callback'): Callback
26 (param: '$router'): Router
27 (param: '$io'): (namespace: string) => PromiseLike<jsonrpc.ws.Client>
28 (param: '$bus'): jsonrpc.ws.Client
29 (param: '$master'): MasterRegistration
30 (param: '$isModule'): IsModule
31 (param: string): any
32}
33
34export interface WorkerInjector extends akala.Injector
35{
36 resolve: resolve;
37}
38const masterPrefixes = ['header', 'route', 'query', 'body'];
39
40export class WorkerInjectorImpl extends akala.Injector
41{
42 constructor(private request: BaseRequest & { body?: any })
43 {
44 super();
45 }
46
47 public resolve<T = any>(name: string): T
48 {
49 let indexOfDot = name.indexOf('.');
50
51 if (~indexOfDot)
52 {
53 let master = name.substr(0, indexOfDot);
54 if (~masterPrefixes.indexOf(master))
55 {
56 log(`resolving ${name}`)
57 switch (master)
58 {
59 case 'header':
60 return this.request.headers[name.substr(indexOfDot + 1)] as any;
61 case 'query':
62 return this.request.query && this.request.query[name.substr(indexOfDot + 1)] as any;
63 case 'route':
64 log(this.request.params)
65 return this.request.params && this.request.params[name.substr(indexOfDot + 1)] as any;
66 case 'body':
67 return this.request.body && this.request.body[name.substr(indexOfDot + 1)];
68 }
69 }
70 }
71 return super.resolve<T>(name);
72 }
73}
74
75export interface Request extends BaseRequest
76{
77 injector?: WorkerInjector;
78 [key: string]: any;
79 body: any;
80}
81
82export interface SendFileOptions extends send.SendOptions
83{
84 headers?: { [key: string]: string }
85}
86
87export function expressWrap(handler: express.Handler)
88{
89 return function (req: Request, next: akala.NextFunction)
90 {
91 var callback = req.injector.resolve('$callback');
92 var headers: any = {};
93 var response = buildResponse(req, callback, next);
94 handler(req as any, response as any, next);
95 }
96}
97
98function buildResponse(req: Request, callback: Callback, next: akala.NextFunction): express.Response
99{
100 return <any>new MyResponse(req, callback, next);
101}
102
103class MyResponse extends stream.Transform implements CallbackResponse
104{
105 constructor(private req: Request, callback: Callback, next: akala.NextFunction)
106 {
107 super({
108 transform: (chunk, _encoding, callback) =>
109 {
110 callback(null, chunk);
111 }, decodeStrings: true
112 });
113 this.headers = {};
114 this.sendStatus = callback
115 this.status = callback
116 this.send = callback
117 this.json = callback
118 this.on('pipe', () =>
119 {
120 this.isStream = true;
121 });
122 this.on('end', () => { this.send(this); });
123 }
124
125 public isStream = false;
126 data: any;
127 headers = {};
128 sendStatus: Callback
129 status: Callback
130 links = undefined
131 send: Callback
132 json: Callback
133 jsonp = undefined
134 sendFile(path: string, options?: SendFileOptions, callback?: akala.NextFunction)
135 {
136 var encodedPath = encodeURI(path);
137 var done = callback;
138 var req = this.req;
139 var res = this;
140 var next = function (err?)
141 {
142 if (err)
143 res.send(500, err);
144 else
145 res.send(200);
146 };
147 var opts: SendFileOptions = options || {};
148
149 if (!path)
150 {
151 throw new TypeError('path argument is required to res.sendFile');
152 }
153
154 // support function as second arg
155 if (typeof options === 'function')
156 {
157 done = options;
158 opts = {};
159 }
160
161 // create file stream
162 var pathname = encodeURI(path);
163 var file = send(req, pathname, opts);
164
165 // transfer
166 sendfile(res, file, opts, function (err)
167 {
168 if (done) return done(err);
169 if (err && err.code === 'EISDIR') return next();
170
171 // next() all but write errors
172 if (err && err.code !== 'ECONNABORTED' && err.syscall !== 'write')
173 {
174 next(err);
175 }
176 });
177 }
178 sendfile = undefined
179 download = undefined
180 contentType(type: string) { this.setHeader('contentType', type); return this; }
181 type = undefined
182 format = undefined
183 attachment = undefined
184 set(field: any): this
185 set(field: string, value?: string): this
186 set(field: string | any, value?: string): this
187 {
188 return this.header(field, value);
189 }
190
191 header(field: any): this
192 header(field: string, value?: string): this
193 header(field: string | any, value?: string): this
194 {
195 if (typeof field == 'string')
196 {
197 if (typeof (value) == 'undefined')
198 return this.headers[field];
199 this.setHeader(field, value);
200 return this;
201 }
202 else
203 {
204 var self = this;
205 if (field)
206 akala.each(field, function (value, key)
207 {
208 self.setHeader(key as string, value);
209 });
210 }
211 }
212 headersSent = false
213 get = undefined
214 clearCookie = undefined
215 cookie = undefined
216 location(location: string) { this.setHeader('location', location); return this; }
217 setHeader(field: string, value: string)
218 {
219 this.headers[field] = value;
220 }
221 /**
222 * Redirect to the given `url` with optional response `status`
223 * defaulting to 302.
224 *
225 * The resulting `url` is determined by `res.location()`, so
226 * it will play nicely with mounted apps, relative paths,
227 * `"back"` etc.
228 *
229 * Examples:
230 *
231 * res.redirect('/foo/bar');
232 * res.redirect('http://example.com');
233 * res.redirect(301, 'http://example.com');
234 * res.redirect('http://example.com', 301);
235 * res.redirect('../login'); // /blog/post/1 -> /blog/login
236 */
237 redirect(url: string): void
238 redirect(status: number, url: string): void
239 redirect(url: string, status: number): void
240 redirect(url: string | number, status?: number | string): void
241 {
242 if (typeof (status) == 'undefined')
243 status = 302
244 if (typeof (url) == 'number' && typeof (status) == 'string')
245 {
246 var swap = url;
247 url = status;
248 status = swap;
249 }
250 this.setHeader('location', <string>url);
251 this.send(status);
252 }
253
254 /**
255 * Render `view` with the given `options` and optional callback `fn`.
256 * When a callback function is given a response will _not_ be made
257 * automatically, otherwise a response of _200_ and _text/html_ is given.
258 *
259 * Options:
260 *
261 * - `cache` boolean hinting to the engine it should cache
262 * - `filename` filename of the view being rendered
263 */
264 render = undefined;
265
266 locals: any;
267
268 charset: string;
269
270 /**
271 * Adds the field to the Vary response header, if it is not there already.
272 * Examples:
273 *
274 * res.vary('User-Agent').render('docs');
275 *
276 */
277 vary = undefined;
278
279 app = undefined;
280 setTimeout = undefined;
281 addTrailers = undefined;
282
283 writeContinue = undefined;
284 finished = false;
285 writable = true;
286 writeHead(statusCode: number, reasonPhrase?: string, headers?: any)
287 writeHead(statusCode: number, headers?: any): void
288 writeHead(statusCode: number, reasonPhrase?: string | any, headers?: any): void
289 {
290 log('writeHead');
291 this.statusCode = statusCode;
292 if (typeof reasonPhrase != 'string')
293 {
294 headers = reasonPhrase;
295 reasonPhrase = null;
296 }
297 if (reasonPhrase)
298 this.statusMessage = reasonPhrase;
299 this.header(headers);
300 this.send(this);
301 this.headersSent = true;
302 }
303 statusCode: number;
304 statusMessage: string;
305 sendDate: boolean;
306 getHeader(name: string)
307 {
308 return this.headers[name];
309 };
310 removeHeader(name: string): void
311 {
312 delete this.headers[name];
313 }
314
315 public _write(chunk: any, encoding?: BufferEncoding, callback?: (err: Error) => void): void
316 {
317 if (!this.headersSent)
318 this.writeHead(this.statusCode);
319
320 super['_write'](chunk, encoding, callback);
321 }
322}
323
324export function handle(app: Router, root: string)
325{
326 return function handle(request: Request, next?: akala.NextFunction): PromiseLike<CallbackResponse>
327 {
328 return new Promise((resolve, reject) =>
329 {
330 var callback: Callback = <any>function callback(status, data?: jsonrpc.PayloadDataType<stream.Readable> | string)
331 {
332 var response: CallbackResponse;
333 if (arguments.length == 0)
334 return reject();
335 if (arguments.length == 1 && status === 'route')
336 return reject(status);
337
338 if (isNaN(Number(status)) || Array.isArray(status))
339 {
340 response = status;
341 if (typeof (data) == 'undefined')
342 {
343 if (typeof (status) == 'undefined')
344 response = { statusCode: 404, data: 'Not found' };
345 else if (isNaN(Number(response.statusCode)) && !(response instanceof stream.Readable))
346 {
347 data = response as any;
348 response = { statusCode: 200, data: data };
349 status = null;
350 }
351 else
352 data = undefined;
353 status = null;
354 }
355 }
356 else
357 response = { statusCode: status, data: 'No data' };
358
359 response.statusCode = response.statusCode || 200;
360
361 if (!(data instanceof stream.Readable) && !Buffer.isBuffer(data) && typeof (data) !== 'string' && typeof data != 'number' && typeof (data) != 'undefined')
362 {
363 if (!response.headers)
364 response.headers = {};
365 if (data instanceof Error)
366 {
367 if (!response.headers['Content-Type'])
368 response.headers['Content-Type'] = 'text/text';
369 data = data.stack;
370 }
371 else
372 {
373 if (!response.headers['Content-Type'])
374 response.headers['Content-Type'] = 'application/json';
375 data = JSON.stringify(data);
376 }
377 }
378 if (typeof (data) != 'undefined')
379 response.data = data;
380
381 resolve(response);
382 }
383
384 callback.redirect = function (url: string)
385 {
386 return callback({ status: 302, headers: { location: url } })
387 }
388
389 var requestInjector: WorkerInjector = new WorkerInjectorImpl(request);
390 requestInjector.register('$request', request);
391 requestInjector.register('$callback', callback);
392 // log(request);
393 Object.defineProperty(request, 'injector', { value: requestInjector, enumerable: false, configurable: false, writable: false });
394
395 log(request.url);
396 app.handle(request, callback, function (err, _req, _callback)
397 {
398 reject(err);
399 });
400 })
401 }
402}
403
404
405function sendfile(res, file, options: SendFileOptions, callback)
406{
407 var done = false;
408 var streaming;
409
410 // request aborted
411 function onaborted()
412 {
413 if (done) return;
414 done = true;
415
416 var err = new Error('Request aborted');
417 err['code'] = 'ECONNABORTED';
418 callback(err);
419 }
420
421 // directory
422 function ondirectory()
423 {
424 if (done) return;
425 done = true;
426
427 var err = new Error('EISDIR, read');
428 err['code'] = 'EISDIR';
429 callback(err);
430 }
431
432 // errors
433 function onerror(err)
434 {
435 if (done) return;
436 done = true;
437 callback(err);
438 }
439
440 // ended
441 function onend()
442 {
443 if (done) return;
444 done = true;
445 callback();
446 }
447
448 // file
449 function onfile()
450 {
451 streaming = false;
452 }
453
454 // finished
455 function onfinish(err)
456 {
457 if (err && err.code === 'ECONNRESET') return onaborted();
458 if (err) return onerror(err);
459 if (done) return;
460
461 setImmediate(function ()
462 {
463 if (streaming !== false && !done)
464 {
465 onaborted();
466 return;
467 }
468
469 if (done) return;
470 done = true;
471 callback();
472 });
473 }
474
475 // streaming
476 function onstream()
477 {
478 streaming = true;
479 }
480
481 file.on('directory', ondirectory);
482 file.on('end', onend);
483 file.on('error', onerror);
484 file.on('file', onfile);
485 file.on('stream', onstream);
486 onFinished(res, onfinish);
487
488 if (options.headers)
489 {
490 // set headers on successful transfer
491 file.on('headers', function headers(res)
492 {
493 var obj = options.headers;
494 var keys = Object.keys(obj);
495
496 for (var i = 0; i < keys.length; i++)
497 {
498 var k = keys[i];
499 res.setHeader(k, obj[k]);
500 }
501 });
502 }
503
504 // pipe
505 file.pipe(res);
506}
\No newline at end of file