Skip to content

Instantly share code, notes, and snippets.

@nicolasparada
Last active September 18, 2023 15:58
Show Gist options
  • Save nicolasparada/bfa29816099c8e8ec25e95152de79abc to your computer and use it in GitHub Desktop.
Save nicolasparada/bfa29816099c8e8ec25e95152de79abc to your computer and use it in GitHub Desktop.
Newline Delimited JSON (NDJSON) Demo
const abortController = new AbortController()
fetch('/api/ndjson', {
headers: { accept: 'application/x-ndjson' },
signal: abortController.signal,
}).then(async res => {
const reader = res.body.getReader()
const textDecoder = new TextDecoder()
try {
while (true) {
const { done, value } = await reader.read()
if (done) {
break
}
const text = textDecoder.decode(value)
const json = JSON.parse(text)
console.log(json)
}
} finally {
reader.releaseLock()
}
}).catch(err => {
if (err.name === 'AbortError') {
return
}
console.error(err)
})
document.querySelector('button').onclick = () => {
abortController.abort()
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NDJSON Demo</title>
<link rel="shortcut icon" href="data:,">
<script src="/client.js" defer></script>
</head>
<body>
<pre>Check console.</pre>
<button>Cancel</button>
</body>
</html>
{
"private": true,
"engines": {
"node": ">= 10.1.0"
},
"scripts": {
"start": "node -r esm server.js"
},
"dependencies": {
"@nicolasparada/httptools": "~0.5.0",
"esm": "^3.0.0",
"serve-handler": "^3.2.0"
}
}
import { createRouter } from '@nicolasparada/httptools';
import * as http from 'http';
import * as process from 'process';
import serve from 'serve-handler';
const router = createRouter()
router.handle('GET', '/api/ndjson', ndjsonHandler)
router.handle('GET', '/*', staticHandler)
const server = http.createServer(router.requestListener)
const port = process.env.PORT || 3000
server.listen(port, err => {
if (err) {
console.error(err)
return
}
console.log(`server running at http://localhost:${port}/`)
})
/**
* @param {http.IncomingMessage} req
* @param {http.ServerResponse} res
*/
async function ndjsonHandler(req, res) {
if (!accepts(req, 'application/x-ndjson')) {
respondText(res, 'Not Acceptable', 406)
return
}
res.setHeader('Content-Type', 'application/x-ndjson')
for await (const now of genEvery(Date.now, 1000)) {
// @ts-ignore
if (req.aborted) {
break
}
const ndjson = JSON.stringify({ now }) + '\n'
res.write(ndjson)
}
res.end()
}
/**
* @param {http.IncomingMessage} req
* @param {http.ServerResponse} res
*/
function staticHandler(req, res) {
serve(req, res, { public: 'static' })
}
/**
* @param {http.IncomingMessage} req
* @param {string} contentType
*/
function accepts(req, contentType) {
if (typeof req.headers.accept !== 'string') {
return false
}
if (req.headers.accept.includes('*/*')) {
return true
}
const [type] = contentType.split('/')
if (req.headers.accept.includes(type + '/*')) {
return true
}
return req.headers.accept.includes(contentType)
}
/**
* @param {http.ServerResponse} res
* @param {string} text
* @param {number=} statusCode
*/
function respondText(res, text, statusCode = 200) {
res.statusCode = statusCode
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
res.end(text)
}
/**
* @param {function(): any} fn
* @param {number=} ms
*/
async function* genEvery(fn, ms = 0) {
yield fn()
while (true) {
await delay(ms)
yield fn()
}
}
/**
* @param {number=} ms
*/
function delay(ms = 0) {
return new Promise(resolve => {
setTimeout(resolve, ms)
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment