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.
@@ -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.0",
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.3.0",
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
- })
@@ -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
- }