1 | import { Request as BaseRequest, WorkerRouter as Router, Callback, CallbackResponse, workerErrorHandler, workerHandler, workerRequestHandler } from './router';
|
2 | import * as akala from '@akala/core';
|
3 | import * as express from 'express';
|
4 | import * as stream from 'stream';
|
5 | export { Router, Callback, workerHandler as RouterHandler, workerErrorHandler as ErrorHandler, workerRequestHandler as RequestHandler };
|
6 | import * as jsonrpc from '@akala/json-rpc-ws'
|
7 | import send from 'send';
|
8 | import onFinished from 'on-finished';
|
9 | const log = akala.log('akala:worker');
|
10 |
|
11 | export { CallbackResponse }
|
12 |
|
13 | export function createClient(namespace: string): PromiseLike<jsonrpc.ws.Client>
|
14 | {
|
15 | return akala.resolve('$agent.' + namespace);
|
16 | }
|
17 |
|
18 | export type MasterRegistration = (from?: string, masterPath?: string, workerPath?: string) => void;
|
19 | export type IsModule = (moduleName: string) => boolean;
|
20 |
|
21 | export 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 |
|
34 | export interface WorkerInjector extends akala.Injector
|
35 | {
|
36 | resolve: resolve;
|
37 | }
|
38 | const masterPrefixes = ['header', 'route', 'query', 'body'];
|
39 |
|
40 | export 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 |
|
75 | export interface Request extends BaseRequest
|
76 | {
|
77 | injector?: WorkerInjector;
|
78 | [key: string]: any;
|
79 | body: any;
|
80 | }
|
81 |
|
82 | export interface SendFileOptions extends send.SendOptions
|
83 | {
|
84 | headers?: { [key: string]: string }
|
85 | }
|
86 |
|
87 | export 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 |
|
98 | function buildResponse(req: Request, callback: Callback, next: akala.NextFunction): express.Response
|
99 | {
|
100 | return <any>new MyResponse(req, callback, next);
|
101 | }
|
102 |
|
103 | class 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 |
|
155 | if (typeof options === 'function')
|
156 | {
|
157 | done = options;
|
158 | opts = {};
|
159 | }
|
160 |
|
161 |
|
162 | var pathname = encodeURI(path);
|
163 | var file = send(req, pathname, opts);
|
164 |
|
165 |
|
166 | sendfile(res, file, opts, function (err)
|
167 | {
|
168 | if (done) return done(err);
|
169 | if (err && err.code === 'EISDIR') return next();
|
170 |
|
171 |
|
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 |
|
223 |
|
224 |
|
225 |
|
226 |
|
227 |
|
228 |
|
229 |
|
230 |
|
231 |
|
232 |
|
233 |
|
234 |
|
235 |
|
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 |
|
256 |
|
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
262 |
|
263 |
|
264 | render = undefined;
|
265 |
|
266 | locals: any;
|
267 |
|
268 | charset: string;
|
269 |
|
270 | |
271 |
|
272 |
|
273 |
|
274 |
|
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 |
|
324 | export 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 |
|
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 |
|
405 | function sendfile(res, file, options: SendFileOptions, callback)
|
406 | {
|
407 | var done = false;
|
408 | var streaming;
|
409 |
|
410 |
|
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 |
|
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 |
|
433 | function onerror(err)
|
434 | {
|
435 | if (done) return;
|
436 | done = true;
|
437 | callback(err);
|
438 | }
|
439 |
|
440 |
|
441 | function onend()
|
442 | {
|
443 | if (done) return;
|
444 | done = true;
|
445 | callback();
|
446 | }
|
447 |
|
448 |
|
449 | function onfile()
|
450 | {
|
451 | streaming = false;
|
452 | }
|
453 |
|
454 |
|
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 |
|
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 |
|
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 |
|
505 | file.pipe(res);
|
506 | } |
\ | No newline at end of file |