diff --git a/AUTHORS b/AUTHORS index 9e29d62..e14a38b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -4,3 +4,4 @@ # some cases, their employer may be the copyright holder. To see the full list # of contributors, see the revision history in source control. Google LLC +Matteo Collina diff --git a/package.json b/package.json index 026f3f7..d6dd0ef 100644 --- a/package.json +++ b/package.json @@ -46,14 +46,15 @@ }, "dependencies": { "cookie": "^0.3.1", - "fastify": "^0.39.1", + "fastify": "^0.43.0", "fastify-plugin": "^0.2.1", - "h2-auto-push": "^0.2.0", - "send": "^0.16.1" + "fastify-static": "^0.7.0", + "h2-auto-push": "^0.2.0" }, "devDependencies": { "@types/cookie": "^0.3.1", "@types/get-port": "^3.2.0", + "@types/node": "^9.4.0", "@types/send": "^0.14.4", "ava": "^0.24.0", "codecov": "^3.0.0", diff --git a/ts/src/fastify-static.d.ts b/ts/src/fastify-static.d.ts new file mode 100644 index 0000000..94f150d --- /dev/null +++ b/ts/src/fastify-static.d.ts @@ -0,0 +1,27 @@ +// Copyright 2018 The node-fastify-auto-push authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +declare module 'fastify-static' { +import * as fastify from 'fastify'; + + interface PluginOptions { + } + + function fastifyStatic( + fn: fastify.Plugin, + options?: PluginOptions| + string): fastify.Plugin; + + export = fastifyStatic; +} diff --git a/ts/src/index.ts b/ts/src/index.ts index 56191a9..2b60bd4 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -18,7 +18,6 @@ import * as autoPush from 'h2-auto-push'; import * as http from 'http'; import * as http2 from 'http2'; import * as https from 'https'; -import * as send from 'send'; import * as stream from 'stream'; import fp = require('fastify-plugin'); @@ -43,60 +42,62 @@ function isHttp2Request(req: RawRequest): req is http2.Http2ServerRequest { return !!(req as http2.Http2ServerRequest).stream; } +function isHttp2Response(res: RawResponse): res is http2.Http2ServerResponse { + return !!(res as http2.Http2ServerResponse).stream; +} + const CACHE_COOKIE_KEY = '__ap_cache__'; -function staticServeFn( +// TODO use a Symbol if ts behaves +interface StorePath extends http2.Http2Stream { + __req_path?: string; +} + +async function staticServeFn( app: fastify.FastifyInstance, - opts: AutoPushOptions, done?: (err?: Error) => void): void { + opts: AutoPushOptions): Promise { const root = opts.root; - let prefix = opts.prefix; + const prefix = opts.prefix || ''; const ap = new autoPush.AutoPush(root, opts.cacheConfig); - if (prefix === undefined) prefix = '/'; - if (prefix[0] !== '/') prefix = '/' + prefix; - if (prefix[prefix.length - 1] !== '/') prefix += '/'; - app.get(prefix + '*', async (req: Request, res: Response) => { - const reqPath: string = prefix + (req.params['*'] || ''); - if (isHttp2Request(req.req)) { - const reqStream = req.req.stream; - const cookies = cookie.parse(req.req.headers['cookie'] as string || ''); + app.register(require('fastify-static'), opts); + + app.addHook('onRequest', async (req, res) => { + if (isHttp2Request(req)) { + const reqStream = req.stream; + const url: string = req.url; + let reqPath: string = url.split('?')[0]; + reqPath = reqPath.replace(prefix, ''); + (req.stream as StorePath).__req_path = reqPath; + const cookies = cookie.parse(req.headers['cookie'] as string || ''); const cacheKey = cookies[CACHE_COOKIE_KEY]; const newCacheKey = await ap.preprocessRequest(reqPath, reqStream, cacheKey); // TODO(jinwoo): Consider making this persistent across sessions. - res.header('set-cookie', cookie.serialize(CACHE_COOKIE_KEY, newCacheKey)); - - send(req.req, reqPath, {root}) - .on('error', - (err) => { - if (err.code === 'ENOENT') { - ap.recordRequestPath(reqStream.session, reqPath, false); - res.code(404).send(); - } else { - res.code(500).send(err); - } - }) - .on('end', - () => { - ap.recordRequestPath(reqStream.session, reqPath, true); - }) - .pipe(res.res as stream.Writable); - await ap.push(reqStream); - } else { - send(req.req, reqPath, {root}) - .on('error', - (err) => { - if (err.code === 'ENOENT') { - res.code(404).send(); - } else { - res.code(500).send(err); - } - }) - .pipe(res.res as stream.Writable); + res.setHeader( + 'set-cookie', cookie.serialize(CACHE_COOKIE_KEY, newCacheKey)); + + ap.push(reqStream).then(noop, noop); } }); - if (done) done(); + app.addHook('onSend', async (request, reply, payload) => { + const res = reply.res; + if (isHttp2Response(res)) { + const resStream = (res as http2.Http2ServerResponse).stream; + const statusCode = (res as http2.Http2ServerResponse).statusCode; + if (statusCode === 404) { + ap.recordRequestPath( + resStream.session, (resStream as StorePath).__req_path || '', + false); + } else if (statusCode < 300 && statusCode >= 200) { + ap.recordRequestPath( + resStream.session, (resStream as StorePath).__req_path || '', true); + } + } + }); } +function noop() {} + export const staticServe = fp(staticServeFn);