This ponyfill library extends the HTTP implementations of Browsers and Nodejs with Braid-HTTP; transforming them from state transfer to state synchronization systems.
These features are provided in an elegant, backwards-compatible way:
- Browsers: get a drop-in replacement for
fetch() - Nodejs: get a route handler that adds abilities to the
http,https, andhttp2modules
It conforms to the Braid-HTTP v04 specification, with the additional HTTP Multiresponse and Multiplexing v1.0 extensions.
Developed in braid.org.
Browsers:
<script src="https://unpkg.com/braid-http/braid-http-client.js"></script>
<script>
// To live on the cutting edge, you can now replace the browser's fetch() if desired:
// window.fetch = braid_fetch
</script>Node.js:
npm install braid-http// Import with require()
require('braid-http').fetch // A polyfill for fetch
require('braid-http').http_client // A polyfill for require('http') clients
require('braid-http').http_server // A polyfill for require('http') servers
// Or as es6 module
import {fetch, http_client, http_server} from 'braid-http'This library adds a {subscribe: true} option to fetch(), and lets you
access the result of a subscription with these new fields on the fetch response:
response.subscribe( update => ... )response.subscription: an iterator that can be used withfor awaitresponse.version: the parsed version from the response headers (if present)
Here is an example of subscribing to a Braid resource using promises:
fetch('https://braid.org/chat', {subscribe: true}).then(
res => res.subscribe(
(update) => {
console.log('We got a new update!', update)
// {
// version: ["me"],
// parents: ["mom", "dad"],
// patches: [{
//. unit: "json",
// range: ".foo",
// content: new Uint8Array([51]),
// content_text: "3" <-- getter
//. }],
// body: new Uint8Array([51]),
// body_text: "3" <-- getter
// }
//
// Note that `update` will contain either patches *or* body
}
)
)(await fetch('/chat', {subscribe: true, retry: true})).subscribe(
(update) => {
// We got a new update!
})var subscription_iterator = (await fetch('/chat',
{subscribe: true, retry: true})).subscription
for await (var update of subscription_iterator) {
// Updates might come in the form of patches:
if (update.patches)
chat = apply_patches(update.patches, chat)
// Or complete snapshots:
else
// Beware the server doesn't send these yet.
chat = JSON.parse(update.body_text)
render_stuff()
}Pass a {retry: true} option to fetch() to automatically reconnect:
fetch('https://braid.org/chat', {subscribe: true, retry: true}).then(
res => res.subscribe(
(update) => {
console.log('We got a new update!', update)
// Do something with the update
}
)
)To update the parent version that you reconnect from, set the parents
paramter to a function rather than an array of strings:
fetch('https://braid.org/chat', {
subscribe: true,
retry: true,
parents: () => current_parents
}).then(
res => res.subscribe(
(update) => {
console.log('We got a new update!', update)
// Do something with the update
}
)
)It will call the parents function each time it reconnects to learn the
current parents to connection from.
The onSubscriptionStatus(status) callback informs you when the connection
goes online and offline:
fetch('https://braid.org/chat', {
subscribe: true,
retry: true,
onSubscriptionStatus: ({online, error, status, statusText}) => {
if (online)
console.log('Connected!')
else
console.log('Disconnected:', error)
}
}).then(
res => res.subscribe(
(update) => { console.log('Got update!', update) }
)
)The callback receives an object with only the fields relevant to the event:
{online: true}— the subscription is connected{online: false, error}— the subscription went offline, with the error/reason for disconnection
sync_resource(url, options) is a higher-level API built on top of
braid_fetch that gives you a reliably-synced subscription plus a PUT queue
that survives network failures. It implements the Reliable Updates
spec: it reconnects
automatically, detects dead connections via heartbeats, retries failed PUTs,
honors Retry-After, warns on unexpected status codes, and aborts on
unrecoverable errors.
var { sync_resource } = require('braid-http')
var ac = new AbortController()
var current_version = []
var s = sync_resource('https://braid.org/chat', {
signal: ac.signal,
parents: () => current_version,
on_update: (update) => {
if (update.version) current_version = update.version
// Apply the update to your local state
},
on_warning: (msg) => console.warn('sync_resource:', msg),
on_error: (err) => console.error('sync_resource shut down:', err)
})
// PUTs are queued and retried automatically. The returned promise
// resolves when the server has acknowledged this specific PUT.
await s.put({
version: ['me-1'],
patches: [{unit: 'text', range: '[0:0]', content: 'hello'}]
})
// Shut everything down (stops the subscription and rejects any
// pending PUTs) by aborting the signal you passed in:
ac.abort()| Option | Default | Description |
|---|---|---|
signal |
— | AbortSignal. When it aborts, the subscription stops, the PUT queue is drained with rejections, and no further retries happen. |
on_update |
— | (update) => .... Called for each update received on the subscription. Same shape as braid_fetch's subscribe callback. |
on_warning |
console.warn |
(msg) => .... Called for unexpected-but-recoverable conditions (e.g. a 500 on a PUT retry, or a parse error that triggers shutdown). |
on_error |
— | (err) => .... Called once when sync_resource shuts itself down due to a fatal condition (e.g. a subscription parse error). Not called when the caller aborts signal. |
parents |
— | Array or callback returning the latest versions the client knows about. Called fresh on every reconnect so the server can resume from the right point. |
headers |
— | Extra HTTP headers to include on every GET and PUT (e.g. Cookie, Authorization, Accept). Per-PUT headers passed through put() override these on conflicts. |
heartbeats |
20 |
Heartbeat period in seconds. Sent as the Heartbeats request header; if the server echoes it back and the client doesn't see any bytes for 1.2 × heartbeats + 3 seconds, it reconnects. |
put_timeout |
heartbeats |
Per-PUT timeout in seconds. If a PUT doesn't complete in time, all in-flight PUTs are aborted and the queue is retried. |
sync_resource() returns { put }:
put(update)— enqueues a PUT.updateis whatever you'd pass tobraid_fetchfor a PUT (e.g.{version, parents, patches}or{version, body}). Returns a promise that resolves with the fetch response when the server acknowledges the PUT, or rejects if the caller'ssignalis aborted first.
Once you've wired up sync_resource, you get the reliable-updates
behaviors for free:
- Auto-reconnection with backoff. First failure retries after 1s; subsequent failures wait 3s. Any successful reconnection resets the counter.
- Status-code handling per spec.
209is a successful subscription response;309,408,425,429,432,502,503,504(and any response carrying aRetry-Afterheader) retry silently; any other non-209status warns viaon_warningand retries. Retry-Afterhonored. If the server sendsRetry-After: N(seconds), the next reconnect waits that long instead of using the default backoff.- Heartbeat liveness detection. The client asks the server to emit
data every
heartbeatsseconds; if it doesn't, the connection is considered dead and gets re-established. - PUT queue with probe-first retries. PUTs are fired in parallel. If any in-flight PUT fails, all siblings are aborted and the queue is retried — starting with a single probe PUT. The rest fan out only after the probe succeeds, avoiding a thundering-herd of doomed PUTs against a still-sick server.
- Per-PUT timeouts. If a PUT hangs longer than
put_timeoutseconds, it's aborted and the queue is retried. - Resume from your latest version. On every reconnect, the
parentscallback is invoked fresh, so the server picks up from wherever your application's state actually is. - Parse-error shutdown. If the subscription stream contains
un-parseable bytes,
sync_resourcewarns, callson_error, and stops retrying — the stream is corrupt and reconnecting won't fix it.
You can braidify your nodejs server with:
var braidify = require('braid-http').http_serverBraidify adds these new abilities to requests and responses:
req.subscribereq.startSubscription({onClose: cb})await req.parseUpdate()res.sendUpdate()
You can call it in two ways:
braidify((req, res) => ...)wraps your HTTP request handler, and gives it perfectly braidified requests and responses.braidify(req, res, next)will add arguments to your existing requests and responses. You can use this as express middleware.
var braidify = require('braid-http').http_server
// or:
import {http_server as braidify} from 'braid-http'
require('http').createServer(
braidify((req, res) => {
// Now braid stuff is available on req and res
// So you can easily handle subscriptions
if (req.subscribe)
res.startSubscription({ onClose: _=> null })
// startSubscription automatically sets statusCode = 209
else
res.statusCode = 200
// And send updates over a subscription
res.sendUpdate({
version: ['greg'],
body: JSON.stringify({greg: 'greg'})
})
})
).listen(9935)If you are working from a library, or from code that does not have access to
the root of the HTTP handler or next in (req, res, next), you can also
call braidify inline:
require('http').createServer(
(req, res) => {
braidify(req, res); if (req.is_multiplexer) return
// Now braid stuff is available on req and res
// ...
})
).listen(9935)This works, but the inline form leaks the multiplexing abstraction in three minor ways.
Or if you're using express, you can just call app.use(braidify) to get
braid features added to every request and response.
var braidify = require('braid-http').http_server
// or:
import {http_server as braidify} from 'braid-http'
var app = require('express')()
app.use(braidify) // Add braid stuff to req and res
app.get('/', (req, res) => {
// Now use it
if (req.subscribe)
res.startSubscription({ onClose: _=> null })
// startSubscription automatically sets statusCode = 209
else
res.statusCode = 200
// Send the current version
res.sendUpdate({
version: ['greg'],
parents: ['gr','eg'],
body: JSON.stringify({greg: 'greg'})
})
// Or you can send patches like this:
// res.sendUpdate({
// version: ['greg'],
// parents: ['gr','eg'],
// patches: [{range: '.greg', unit: 'json', content: '"greg"'}]
// })
})
require('http').createServer(app).listen(8583)// Use this line if necessary for self-signed certs
// process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0
var https = require('braid-http').http_client(require('https'))
// or:
// import braid_http from 'braid-http'
// https = braid_http.http_client(require('https'))
https.get(
'https://braid.org/chat',
{subscribe: true},
(res) => {
res.on('update', (update) => {
console.log('well we got one', update)
})
}
)To get auto-reconnections use:
function connect () {
https.get(
'https://braid.org/chat',
{subscribe: true},
(res) => {
res.on('update', (update) => {
// {
// version: ["me"],
// parents: ["mom", "dad"],
// patches: [{
//. unit: "json",
// range: ".foo",
// content: new Uint8Array([51]),
// content_text: "3" <-- getter
//. }],
// body: new Uint8Array([51]),
// body_text: "3" <-- getter
// }
// Update will contain either patches *or* body, but not both
console.log('We got a new update!', update)
})
res.on('end', e => setTimeout(connect, 1000))
res.on('error', e => setTimeout(connect, 1000))
})
}
connect()var fetch = require('braid-http').fetch
// or:
import {fetch} from 'braid-http'
// process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0
fetch('https://localhost:3009/chat',
{subscribe: true}).andThen(
x => console.log('Got ', x)
)Run all tests from the command line:
npm testRun tests in a browser (auto-opens):
npm run test:browserYou can also filter tests by name:
node test/test.js --filter="version"This library automatically
multiplexes subscriptions behind
the scenes to overcome web browsers' 6-connection limit (with HTTP/1) and
100-connection limit (with HTTP/2). When you setup a server's braidify in
the recommended ways, you don't need to know it's happening — the abstraction
is completely transparent.
// Recommendation #1: Wrapping the entire request handler
require('http').createServer(
braidify((req, res) => {
...
})
)// Recommendation #2: As middleware
var app = require('express')()
app.use(braidify)// Recommendation #3: With braidify(req, res, next)
// (Equivalent to the middleware form.)
app.use(
(req, res, next) => {
...
braidify(req, res, next)
...
}
)If you are using braidify from within a library, or in another context without
access to the entire request handler, or a next() method, then you can use
the inline braidify(req, res) form:
require('http').createServer(
(req, res) => {
...
braidify(req, res); if (req.is_multiplexer) return
...
}
)Just know that there are three abstraction leaks when using this form:
- You must add
if (req.is_multiplexer) returnafter thebraidify(req, res)call. - The library will disable the buffering optimization on optimistic multiplexer creation. This buffering prevents two minor inconveniences that occur on about ~15% of page loads:
The buffering works like this: when the client connects to a new host, it
sends a POST to create the multiplexer and GETs to subscribe — all in
parallel. Sometimes a GET arrives before the POST. With the recommended
forms, the server briefly buffers the GET (70ms, event-driven) until the
POST lands, then processes it normally. Without next, the server can't
re-run the handler, so it returns 424 immediately and the client retries.
You can tune multiplexing on the client, per-request or globally:
braid_fetch('/a', {multiplex: true}) // force on for this request
braid_fetch('/a', {multiplex: false}) // force off for this request
braid_fetch.enable_multiplex = true // on for all GETs
braid_fetch.enable_multiplex = false // off globally
braid_fetch.enable_multiplex = {after: 1} // on after N connections (default)And on the server:
braidify.enable_multiplex = true // default; set false to disable
braidify.multiplex_wait = 10 // ms; timeout for the buffering optimization (default 10)
// set to 0 to disable buffering