fastify-ata 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +23 -0
- package/.github/workflows/release.yml +45 -0
- package/package.json +2 -2
- package/bench-realtime.js +0 -131
- package/bench-turbo.js +0 -118
- package/profile-fastify.mjs +0 -87
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
node: [20, 22, 24]
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: actions/setup-node@v4
|
|
20
|
+
with:
|
|
21
|
+
node-version: ${{ matrix.node }}
|
|
22
|
+
- run: npm install
|
|
23
|
+
- run: npm test
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishing a GitHub release publishes the same version to npm via trusted
|
|
4
|
+
# publishing (OIDC). No tokens: the trusted publisher for this repo and
|
|
5
|
+
# workflow must be configured on npmjs.com for the package.
|
|
6
|
+
on:
|
|
7
|
+
release:
|
|
8
|
+
types: [published]
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: 22
|
|
21
|
+
- run: npm install
|
|
22
|
+
- run: npm test
|
|
23
|
+
|
|
24
|
+
publish:
|
|
25
|
+
needs: test
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
permissions:
|
|
28
|
+
contents: read
|
|
29
|
+
id-token: write
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v4
|
|
32
|
+
- uses: actions/setup-node@v4
|
|
33
|
+
with:
|
|
34
|
+
node-version: 22
|
|
35
|
+
- run: npm install -g npm@latest
|
|
36
|
+
- name: Refuse a version that does not match the tag
|
|
37
|
+
run: |
|
|
38
|
+
TAG="${GITHUB_REF_NAME#v}"
|
|
39
|
+
PKG="$(node -p "require('./package.json').version")"
|
|
40
|
+
if [ "$TAG" != "$PKG" ]; then
|
|
41
|
+
echo "tag $TAG does not match package.json $PKG" >&2
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
- run: npm install
|
|
45
|
+
- run: npm publish
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastify-ata",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "Fastify plugin for ata-validator. Runtime-competitive with the default, 24x faster serverless cold start, Standard Schema V1.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://github.com/ata-core/fastify-ata#readme",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"ata-validator": "^1.
|
|
29
|
+
"ata-validator": "^1.7.2",
|
|
30
30
|
"fastify-plugin": "^5.1.0",
|
|
31
31
|
"sanitize-filename": "^1.6.4"
|
|
32
32
|
},
|
package/bench-realtime.js
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
// Real-time Fastify simulation: actual HTTP requests with autocannon
|
|
4
|
-
// Shows: startup time + request throughput + latency
|
|
5
|
-
|
|
6
|
-
const { writeFileSync, unlinkSync } = require('fs')
|
|
7
|
-
const { fork, execSync } = require('child_process')
|
|
8
|
-
const path = require('path')
|
|
9
|
-
|
|
10
|
-
const ROUTES = 50
|
|
11
|
-
const DURATION = 5 // seconds
|
|
12
|
-
const CONNECTIONS = 50
|
|
13
|
-
|
|
14
|
-
function makeRoutes(count) {
|
|
15
|
-
return Array.from({ length: count }, (_, i) => ({
|
|
16
|
-
path: `/api/v1/resource${i}`,
|
|
17
|
-
schema: {
|
|
18
|
-
body: {
|
|
19
|
-
type: 'object',
|
|
20
|
-
properties: {
|
|
21
|
-
id: { type: 'integer', minimum: 1 },
|
|
22
|
-
name: { type: 'string', minLength: 1, maxLength: 100 },
|
|
23
|
-
email: { type: 'string', format: 'email' },
|
|
24
|
-
age: { type: 'integer', minimum: 0, maximum: 150 },
|
|
25
|
-
active: { type: 'boolean' },
|
|
26
|
-
role: { enum: ['admin', 'user', 'moderator'] },
|
|
27
|
-
},
|
|
28
|
-
required: ['id', 'name', 'email', 'active', 'role'],
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
}))
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const routes = makeRoutes(ROUTES)
|
|
35
|
-
const payload = JSON.stringify({
|
|
36
|
-
id: 42, name: 'Mert', email: 'mert@example.com',
|
|
37
|
-
age: 26, active: true, role: 'admin'
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
function serverScript(useAta) {
|
|
41
|
-
return `
|
|
42
|
-
'use strict'
|
|
43
|
-
const t0 = process.hrtime.bigint()
|
|
44
|
-
const fastify = require('fastify')()
|
|
45
|
-
${useAta ? "fastify.register(require('./index'))" : ''}
|
|
46
|
-
const routes = ${JSON.stringify(routes)}
|
|
47
|
-
for (const r of routes) {
|
|
48
|
-
fastify.post(r.path, { schema: r.schema }, (req, reply) => {
|
|
49
|
-
reply.send({ ok: true, id: req.body.id })
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
fastify.listen({ port: 0 }).then(() => {
|
|
53
|
-
const startupMs = Number(process.hrtime.bigint() - t0) / 1e6
|
|
54
|
-
const port = fastify.server.address().port
|
|
55
|
-
process.send({ port, startupMs })
|
|
56
|
-
})
|
|
57
|
-
`
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function runTest(label, useAta) {
|
|
61
|
-
const scriptPath = path.join(__dirname, '_realtime_server.js')
|
|
62
|
-
writeFileSync(scriptPath, serverScript(useAta))
|
|
63
|
-
|
|
64
|
-
const child = fork(scriptPath, { stdio: ['pipe', 'pipe', 'pipe', 'ipc'] })
|
|
65
|
-
const { port, startupMs } = await new Promise(resolve => {
|
|
66
|
-
child.on('message', msg => resolve(msg))
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
// Pick a random route for each request
|
|
70
|
-
const targetRoute = routes[Math.floor(Math.random() * routes.length)].path
|
|
71
|
-
const escaped = payload.replace(/'/g, "'\\''")
|
|
72
|
-
|
|
73
|
-
let result
|
|
74
|
-
try {
|
|
75
|
-
const raw = execSync(
|
|
76
|
-
`npx autocannon -c ${CONNECTIONS} -d ${DURATION} -j http://localhost:${port}${targetRoute} -m POST -H "content-type: application/json" -b '${escaped}'`,
|
|
77
|
-
{ cwd: __dirname, timeout: 30000 }
|
|
78
|
-
).toString()
|
|
79
|
-
result = JSON.parse(raw)
|
|
80
|
-
} catch (e) {
|
|
81
|
-
child.kill()
|
|
82
|
-
try { unlinkSync(scriptPath) } catch {}
|
|
83
|
-
return null
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
child.kill()
|
|
87
|
-
try { unlinkSync(scriptPath) } catch {}
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
label,
|
|
91
|
-
startupMs,
|
|
92
|
-
reqPerSec: result.requests.average,
|
|
93
|
-
latencyAvg: result.latency.average,
|
|
94
|
-
latencyP99: result.latency.p99,
|
|
95
|
-
throughputMB: (result.throughput.average / 1024 / 1024).toFixed(1),
|
|
96
|
-
errors: result.errors,
|
|
97
|
-
timeouts: result.timeouts,
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function main() {
|
|
102
|
-
console.log('\n' + '='.repeat(60))
|
|
103
|
-
console.log(' Real-Time Fastify Simulation')
|
|
104
|
-
console.log(` ${ROUTES} routes, ${CONNECTIONS} connections, ${DURATION}s duration`)
|
|
105
|
-
console.log(' POST with 6-field validated body')
|
|
106
|
-
console.log('='.repeat(60) + '\n')
|
|
107
|
-
|
|
108
|
-
const ata = await runTest('fastify-ata', true)
|
|
109
|
-
const ajv = await runTest('fastify (ajv)', false)
|
|
110
|
-
|
|
111
|
-
if (!ata || !ajv) { console.log('Benchmark failed'); return }
|
|
112
|
-
|
|
113
|
-
console.log(' fastify-ata fastify (ajv)')
|
|
114
|
-
console.log(' ─────────────────────────────────────────────────')
|
|
115
|
-
console.log(` Startup ${ata.startupMs.toFixed(1).padStart(8)} ms ${ajv.startupMs.toFixed(1).padStart(8)} ms`)
|
|
116
|
-
console.log(` Requests/sec ${ata.reqPerSec.toLocaleString().padStart(8)} ${ajv.reqPerSec.toLocaleString().padStart(8)}`)
|
|
117
|
-
console.log(` Latency (avg) ${ata.latencyAvg.toFixed(2).padStart(8)} ms ${ajv.latencyAvg.toFixed(2).padStart(8)} ms`)
|
|
118
|
-
console.log(` Latency (p99) ${ata.latencyP99.toFixed(2).padStart(8)} ms ${ajv.latencyP99.toFixed(2).padStart(8)} ms`)
|
|
119
|
-
console.log(` Throughput ${ata.throughputMB.padStart(8)} MB/s ${ajv.throughputMB.padStart(8)} MB/s`)
|
|
120
|
-
console.log(` Errors ${String(ata.errors).padStart(8)} ${String(ajv.errors).padStart(8)}`)
|
|
121
|
-
console.log(` Timeouts ${String(ata.timeouts).padStart(8)} ${String(ajv.timeouts).padStart(8)}`)
|
|
122
|
-
console.log()
|
|
123
|
-
|
|
124
|
-
const startupRatio = ajv.startupMs / ata.startupMs
|
|
125
|
-
const rpsRatio = ata.reqPerSec / ajv.reqPerSec
|
|
126
|
-
console.log(` Startup: ata ${startupRatio.toFixed(1)}x faster`)
|
|
127
|
-
console.log(` Throughput: ata ${rpsRatio.toFixed(2)}x ${rpsRatio >= 1 ? 'faster' : 'slower'}`)
|
|
128
|
-
console.log()
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
main().catch(err => { console.error(err); process.exit(1) })
|
package/bench-turbo.js
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { execSync } = require('child_process')
|
|
4
|
-
const { writeFileSync, unlinkSync } = require('fs')
|
|
5
|
-
const path = require('path')
|
|
6
|
-
|
|
7
|
-
const schema = {
|
|
8
|
-
body: {
|
|
9
|
-
type: 'object',
|
|
10
|
-
properties: {
|
|
11
|
-
users: {
|
|
12
|
-
type: 'array',
|
|
13
|
-
items: {
|
|
14
|
-
type: 'object',
|
|
15
|
-
properties: {
|
|
16
|
-
id: { type: 'integer', minimum: 1 },
|
|
17
|
-
name: { type: 'string', minLength: 1 },
|
|
18
|
-
email: { type: 'string', format: 'email' },
|
|
19
|
-
age: { type: 'integer', minimum: 0, maximum: 150 },
|
|
20
|
-
active: { type: 'boolean' },
|
|
21
|
-
role: { enum: ['admin', 'user', 'moderator'] },
|
|
22
|
-
},
|
|
23
|
-
required: ['id', 'name', 'email', 'active', 'role'],
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
metadata: {
|
|
27
|
-
type: 'object',
|
|
28
|
-
properties: {
|
|
29
|
-
total: { type: 'integer' },
|
|
30
|
-
page: { type: 'integer', minimum: 1 },
|
|
31
|
-
},
|
|
32
|
-
required: ['total', 'page'],
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
required: ['users', 'metadata'],
|
|
36
|
-
},
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function makePayload(n) {
|
|
40
|
-
const users = []
|
|
41
|
-
for (let i = 0; i < n; i++) {
|
|
42
|
-
users.push({
|
|
43
|
-
id: i + 1,
|
|
44
|
-
name: `User ${i}`,
|
|
45
|
-
email: `user${i}@example.com`,
|
|
46
|
-
age: 25,
|
|
47
|
-
active: true,
|
|
48
|
-
role: 'user',
|
|
49
|
-
})
|
|
50
|
-
}
|
|
51
|
-
return JSON.stringify({ users, metadata: { total: n, page: 1 } })
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function serverScript(turbo) {
|
|
55
|
-
return `
|
|
56
|
-
'use strict'
|
|
57
|
-
const fastify = require('fastify')()
|
|
58
|
-
fastify.register(require('./index'), { turbo: ${turbo} })
|
|
59
|
-
const schema = ${JSON.stringify(schema)}
|
|
60
|
-
fastify.post('/users', { schema }, (req, reply) => {
|
|
61
|
-
reply.send({ ok: true, count: req.body.users.length })
|
|
62
|
-
})
|
|
63
|
-
fastify.listen({ port: 0 }).then(() => {
|
|
64
|
-
process.send({ port: fastify.server.address().port })
|
|
65
|
-
})
|
|
66
|
-
`
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function bench(label, turbo, payload) {
|
|
70
|
-
const scriptPath = path.join(__dirname, '_bench_turbo_server.js')
|
|
71
|
-
writeFileSync(scriptPath, serverScript(turbo))
|
|
72
|
-
|
|
73
|
-
const { fork } = require('child_process')
|
|
74
|
-
const child = fork(scriptPath, { stdio: ['pipe', 'pipe', 'pipe', 'ipc'] })
|
|
75
|
-
const port = await new Promise((resolve) => {
|
|
76
|
-
child.on('message', (msg) => resolve(msg.port))
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
const escaped = payload.replace(/'/g, "'\\''")
|
|
80
|
-
const result = execSync(
|
|
81
|
-
`npx autocannon -c 10 -d 5 -j http://localhost:${port}/users -m POST -H "content-type: application/json" -b '${escaped}'`,
|
|
82
|
-
{ cwd: __dirname, timeout: 30000 },
|
|
83
|
-
).toString()
|
|
84
|
-
|
|
85
|
-
child.kill()
|
|
86
|
-
try { unlinkSync(scriptPath) } catch {}
|
|
87
|
-
|
|
88
|
-
const parsed = JSON.parse(result)
|
|
89
|
-
return parsed.requests.average
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function main() {
|
|
93
|
-
console.log('\n==============================================')
|
|
94
|
-
console.log(' Turbo Mode Benchmark: normal vs turbo')
|
|
95
|
-
console.log(' 10 connections, 5 seconds, real HTTP')
|
|
96
|
-
console.log('==============================================\n')
|
|
97
|
-
|
|
98
|
-
for (const count of [1, 10, 50, 100]) {
|
|
99
|
-
const payload = makePayload(count)
|
|
100
|
-
console.log(`--- ${count} users (${(payload.length / 1024).toFixed(1)} KB) ---`)
|
|
101
|
-
|
|
102
|
-
const normal = await bench('normal', false, payload)
|
|
103
|
-
const turbo = await bench('turbo', true, payload)
|
|
104
|
-
|
|
105
|
-
console.log(` normal: ${normal.toLocaleString().padStart(10)} req/sec`)
|
|
106
|
-
console.log(` turbo: ${turbo.toLocaleString().padStart(10)} req/sec`)
|
|
107
|
-
|
|
108
|
-
const ratio = turbo / normal
|
|
109
|
-
if (ratio >= 1) console.log(` >>> turbo ${ratio.toFixed(2)}x faster`)
|
|
110
|
-
else console.log(` >>> normal ${(1 / ratio).toFixed(2)}x faster`)
|
|
111
|
-
console.log()
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
main().catch((err) => {
|
|
116
|
-
console.error(err)
|
|
117
|
-
process.exit(1)
|
|
118
|
-
})
|
package/profile-fastify.mjs
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
// Profile validation overhead in Fastify context
|
|
4
|
-
import { createRequire } from 'module'
|
|
5
|
-
const require = createRequire(import.meta.url)
|
|
6
|
-
|
|
7
|
-
const { Validator } = require('ata-validator')
|
|
8
|
-
|
|
9
|
-
// Simulate what Fastify does: JSON.parse + validate
|
|
10
|
-
const schema = {
|
|
11
|
-
type: 'object',
|
|
12
|
-
properties: {
|
|
13
|
-
users: {
|
|
14
|
-
type: 'array',
|
|
15
|
-
items: {
|
|
16
|
-
type: 'object',
|
|
17
|
-
properties: {
|
|
18
|
-
id: { type: 'integer', minimum: 1 },
|
|
19
|
-
name: { type: 'string', minLength: 1 },
|
|
20
|
-
email: { type: 'string', format: 'email' },
|
|
21
|
-
age: { type: 'integer', minimum: 0, maximum: 150 },
|
|
22
|
-
active: { type: 'boolean' },
|
|
23
|
-
role: { enum: ['admin', 'user', 'moderator'] },
|
|
24
|
-
},
|
|
25
|
-
required: ['id', 'name', 'email', 'active', 'role'],
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
metadata: {
|
|
29
|
-
type: 'object',
|
|
30
|
-
properties: {
|
|
31
|
-
total: { type: 'integer' },
|
|
32
|
-
page: { type: 'integer', minimum: 1 },
|
|
33
|
-
},
|
|
34
|
-
required: ['total', 'page'],
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
required: ['users', 'metadata'],
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function makePayload(n) {
|
|
41
|
-
const users = []
|
|
42
|
-
for (let i = 0; i < n; i++) {
|
|
43
|
-
users.push({ id: i+1, name: `User ${i}`, email: `u${i}@x.com`, age: 25, active: true, role: 'user' })
|
|
44
|
-
}
|
|
45
|
-
return JSON.stringify({ users, metadata: { total: n, page: 1 } })
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const v = new Validator(schema)
|
|
49
|
-
const payloads = [1, 10, 50, 100].map(n => ({ n, json: makePayload(n) }))
|
|
50
|
-
|
|
51
|
-
// Warmup
|
|
52
|
-
for (const p of payloads) {
|
|
53
|
-
for (let i = 0; i < 1000; i++) {
|
|
54
|
-
const obj = JSON.parse(p.json)
|
|
55
|
-
v.validate(obj)
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
console.log('Breakdown: JSON.parse vs validate vs total\n')
|
|
60
|
-
|
|
61
|
-
const N = 100000
|
|
62
|
-
for (const p of payloads) {
|
|
63
|
-
// JSON.parse only
|
|
64
|
-
let t = process.hrtime.bigint()
|
|
65
|
-
for (let i = 0; i < N; i++) JSON.parse(p.json)
|
|
66
|
-
const dtParse = Number(process.hrtime.bigint() - t) / 1e6
|
|
67
|
-
|
|
68
|
-
// validate only (pre-parsed)
|
|
69
|
-
const obj = JSON.parse(p.json)
|
|
70
|
-
t = process.hrtime.bigint()
|
|
71
|
-
for (let i = 0; i < N; i++) v.validate(obj)
|
|
72
|
-
const dtValidate = Number(process.hrtime.bigint() - t) / 1e6
|
|
73
|
-
|
|
74
|
-
// parse + validate
|
|
75
|
-
t = process.hrtime.bigint()
|
|
76
|
-
for (let i = 0; i < N; i++) { const o = JSON.parse(p.json); v.validate(o) }
|
|
77
|
-
const dtTotal = Number(process.hrtime.bigint() - t) / 1e6
|
|
78
|
-
|
|
79
|
-
const parsePct = (dtParse / dtTotal * 100).toFixed(0)
|
|
80
|
-
const valPct = (dtValidate / dtTotal * 100).toFixed(0)
|
|
81
|
-
|
|
82
|
-
console.log(`${p.n} users (${(p.json.length/1024).toFixed(1)}KB):`)
|
|
83
|
-
console.log(` JSON.parse: ${(dtParse/N*1000).toFixed(1)} us (${parsePct}%)`)
|
|
84
|
-
console.log(` validate: ${(dtValidate/N*1000).toFixed(1)} us (${valPct}%)`)
|
|
85
|
-
console.log(` total: ${(dtTotal/N*1000).toFixed(1)} us`)
|
|
86
|
-
console.log()
|
|
87
|
-
}
|