Skip to content

Commit 39f342e

Browse files
authored
feat!: v1 (#1)
1 parent b4f93ab commit 39f342e

File tree

7 files changed

+548
-8
lines changed

7 files changed

+548
-8
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ jobs:
66
runs-on: ubuntu-latest
77
strategy:
88
matrix:
9-
node: [14.x, 16.x, 17.x]
9+
node: [16.x, 18.x]
1010
name: Node ${{ matrix.node }}
1111
steps:
1212
- uses: actions/checkout@v1

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,6 @@ dist
102102

103103
# TernJS port file
104104
.tern-port
105+
106+
# Not necessary for libs
107+
package-lock.json

index.js

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,163 @@
11
'use strict'
2+
const fp = require('fastify-plugin')
3+
4+
const { Errors } = require('./lib/index')
5+
6+
module.exports = fp(
7+
function fastifyRacePlugin (fastify, globalOpts, next) {
8+
const controllers = new WeakMap()
9+
let error
10+
11+
if (globalOpts != null && typeof globalOpts !== 'object') {
12+
return next(new Errors.BAD_PARAMS('object', typeof globalOpts))
13+
}
14+
15+
globalOpts = Object.assign(
16+
{},
17+
{ handleOnError: true, onRequestClosed: null },
18+
globalOpts
19+
)
20+
21+
if (typeof globalOpts.handleOnError !== 'boolean') {
22+
error = new Errors.BAD_PARAMS('boolean', typeof globalOpts.handleOnError)
23+
} else if (
24+
globalOpts.onRequestClosed != null &&
25+
typeof globalOpts.onRequestClosed !== 'function'
26+
) {
27+
error = new Errors.BAD_PARAMS(
28+
'function',
29+
typeof globalOpts.onRequestClosed
30+
)
31+
}
32+
33+
fastify.decorateRequest('race', race)
34+
fastify.addHook('onResponse', fastifyRacingCleaner)
35+
36+
return next(error)
37+
38+
function fastifyRacingCleaner (request, _reply, done) {
39+
if (controllers.has(request)) {
40+
const { controller, cbs } = controllers.get(request)
41+
42+
if (controller.signal.aborted === false) {
43+
for (const cb of cbs) {
44+
controller.signal.removeEventListener('abort', cb, {
45+
once: true
46+
})
47+
}
48+
}
49+
50+
controllers.delete(request)
51+
}
52+
53+
done()
54+
}
55+
56+
function race (opts = globalOpts) {
57+
const { raw, id: reqId } = this
58+
const handleError = typeof opts === 'function' ? true : opts.handleOnError
59+
const cb = typeof opts === 'function' ? opts : opts.onRequestClosed
60+
61+
if (controllers.has(this)) {
62+
const { controller: ctrl, cbs } = controllers.get(this)
63+
64+
if (ctrl.signal.aborted === true) {
65+
throw new Errors.ALREADY_ABORTED(reqId)
66+
}
67+
68+
if (raw.socket.destroyed === true) {
69+
throw new Errors.SOCKET_CLOSED(reqId)
70+
}
71+
72+
if (cb != null) {
73+
ctrl.signal.addEventListener('abort', cb, {
74+
once: true
75+
})
76+
77+
controllers.set(this, { controller: ctrl, cbs: cbs.concat(cb) })
78+
}
79+
80+
return ctrl.signal
81+
} else {
82+
// eslint-disable-next-line no-undef
83+
const controller = new AbortController()
84+
85+
if (cb != null) {
86+
controller.signal.addEventListener('abort', cb, {
87+
once: true
88+
})
89+
}
90+
91+
if (cb == null) controller.signal.then = theneable.bind(this)
92+
93+
if (raw.socket.destroyed) {
94+
throw new Errors.SOCKET_CLOSED(reqId)
95+
} else {
96+
raw.once(
97+
'close',
98+
function () {
99+
if (controllers.has(this)) {
100+
const { controller: ctrl } = controllers.get(this)
101+
if (ctrl.signal.aborted === false) controller.abort()
102+
}
103+
}.bind(this)
104+
)
105+
106+
if (handleError === true || cb != null) {
107+
raw.once(
108+
'error',
109+
function (err) {
110+
if (controllers.has(this)) {
111+
const { controller: ctrl } = controllers.get(this)
112+
if (ctrl.signal.aborted === false) controller.abort(err)
113+
}
114+
}.bind(this)
115+
)
116+
}
117+
}
118+
119+
controllers.set(this, { controller, cbs: cb != null ? [cb] : [] })
120+
121+
return controller.signal
122+
}
123+
124+
function theneable (resolve, reject) {
125+
const { controller, cbs } = controllers.get(this)
126+
127+
if (raw.socket.destroyed === true) {
128+
return reject(Errors.SOCKET_CLOSED(this.id))
129+
}
130+
131+
if (controller.signal.aborted === true) {
132+
return reject(Errors.ALREADY_ABORTED(this.id))
133+
}
134+
135+
try {
136+
controller.signal.addEventListener('abort', theneableHandler, {
137+
once: true
138+
})
139+
140+
controllers.set(this, {
141+
controller,
142+
cbs: cbs.concat(theneableHandler)
143+
})
144+
} catch (err) {
145+
reject(err)
146+
}
147+
148+
function theneableHandler (evt) {
149+
const event = {
150+
type: evt.type,
151+
reason: controller.signal?.reason
152+
}
153+
154+
resolve(event)
155+
}
156+
}
157+
}
158+
},
159+
{
160+
fastify: '>=3.24.1',
161+
name: 'fastify-racing'
162+
}
163+
)

lib/index.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const Errors = require('@fastify/error')
2+
3+
module.exports = {
4+
Errors: {
5+
BAD_PARAMS: Errors(
6+
'FST_PLUGIN_RACE_BAD_PARAM',
7+
'Invalid param, expected %s but received %s'
8+
),
9+
ALREADY_ABORTED: Errors(
10+
'FST_PLUGIN_RACE_ALREADY_ABORTED',
11+
"Request with ID '%s' already aborted"
12+
),
13+
SOCKET_CLOSED: Errors(
14+
'FST_PLUGIN_RACE_SOCKET_CLOSED',
15+
"Socket for request with ID '%s' already closed"
16+
)
17+
}
18+
}

package.json

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"name": "<lib_name>",
2+
"name": "fastify-racing",
33
"version": "0.0.0",
4-
"description": "<description>",
4+
"description": "Cancel any running operation at the right time on your request handler",
55
"main": "index.js",
66
"types": "index.d.ts",
77
"scripts": {
@@ -13,27 +13,35 @@
1313
"lint:ci": "standard",
1414
"typescript": "tsd"
1515
},
16+
"engines": {
17+
"node": ">=16.0.0"
18+
},
1619
"keywords": [],
1720
"repository": {
1821
"type": "git",
19-
"url": "git+https://github.com/metcoder95/<lib_name>.git"
22+
"url": "git+https://github.com/metcoder95/fastify-racing.git"
2023
},
21-
"readme": "https://github.com/metcoder95/<lib_name>/blob/main/README.md",
24+
"readme": "https://github.com/metcoder95/fastify-racing/blob/main/README.md",
2225
"bugs": {
23-
"url": "https://github.com/metcoder95/<lib_name>/issues"
26+
"url": "https://github.com/metcoder95/fastify-racing/issues"
2427
},
2528
"author": "metcoder95 <me@metcoder.dev>",
2629
"license": "MIT",
2730
"devDependencies": {
28-
"@types/node": "^14.17.6",
31+
"@types/node": "^14.18.13",
32+
"fastify": "^3.28.0",
2933
"husky": "^7.0.2",
34+
"nodemon": "^2.0.15",
3035
"snazzy": "^9.0.0",
3136
"standard": "^16.0.3",
3237
"tap": "^15.0.10",
3338
"tsd": "^0.17.0",
34-
"typescript": "^4.4"
39+
"typescript": "^4.4",
40+
"undici": "^5.0.0"
3541
},
3642
"dependencies": {
43+
"@fastify/error": "^2.0.0",
44+
"fastify-plugin": "^3.0.1"
3745
},
3846
"tsd": {
3947
"directory": "test"

test/index.test-d.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/// <reference types="node" />
2+
import { FastifyPluginCallback } from 'fastify';
3+
import { FastifyError } from '@fastify/error';
4+
5+
6+
interface AbortEvent {
7+
type: 'abort' | string;
8+
reason?: FastifyError | Error
9+
}
10+
11+
interface FastifyRacing {
12+
handleError?: boolean;
13+
onRequestClosed?: (evt: AbortEvent) => void;
14+
}
15+
16+
declare module 'fastify' {
17+
interface FastifyInstance {
18+
race(cb: FastifyRacing['onRequestClosed']): void
19+
race(opts: FastifyRacing): Promise<AbortEvent>
20+
race(): Promise<AbortEvent>
21+
}
22+
}
23+
24+
declare const FastifyRacing: FastifyPluginCallback<FastifyRacing>;
25+
26+
export default FastifyRacing;

0 commit comments

Comments
 (0)