This library is a utility library for synchronising automerge documents over HTTP using a application/x-ndjson
protocol:
- Both the request and response bodies contain newline-delimited json lines. The content-type is
application/x-ndjson; charset=utf-8
. - Each line looks like
{"event":"sync", "data":"<base64-encoded sync message>"}\n
- The server stays connected, continuously receiving messages and sending messages as they are ready on the document via either HTTP2 or well-behaved HTTP1.1 clients.
- The client decides when to terminate the connection by observing the messages it receives, either:
- The response body is closed after the server detects that the request body is complete and no more messages are available.
- The client sees a sync message that meets its "termination check", which may indicate that the server matches the local state or that the local state contains all the remote head nodes. This can be used for local tools that need to perform a "one-shot" synchronisation on startup.
- There's a broadcast capability that allows a server to serve changes from multiple clients on the same doc simultaneously or for a client to synchronise with multiple servers.
- The client supports HTTP redirect behavior so that servers can implement rudimentary partitioning and balancing of requests.
This library will be used to build a series of small peer-to-peer and distributed state utilities built on Automerge. The protocol above is easy to replicate in most languages, most importantly Go (in this repo) and Javascript.
The Automerge sync server https://github.com/automerge/automerge-repo-sync-server and related https://github.com/automerge/automerge-connection libraries are written in and generally use websockets to communicate. I wanted to try and utilise HTTP2 and concurrent HTTP1.1 to achieve a similar thing with Go.
This gives me a pure-go option with very few dependencies that I can trust to be stable and maintainable for a long time.
By executing go run ./examples/server/
in one terminal, and DOC_ID=example go run ./examples/http2writer
in another terminal, I can then execute a raw curl
request in a 3rd terminal to follow the stream over HTTPS. I send an empty sync message to start and observe the following:
$ curl -k -v -X PUT https://localhost:8080/example -d '{"event":"sync","data":"QgAAAQAAAA=="}' -H 'Content-Type: application/x-ndjson'
...
> PUT /example HTTP/2
> Host: localhost:8080
> User-Agent: curl/8.5.0
> Accept: */*
> Content-Type: application/x-ndjson
> Content-Length: 38
< HTTP/2 200
< content-type: application/x-ndjson; charset=utf-8
< date: Sat, 26 Oct 2024 09:56:23 GMT
<
{"event":"sync","data":"QgFbkqa2LT<snip>9CqZQD6wA="}
{"event":"sync","data":"QgEVJTDZHR<snip>F/AH+BAQ=="}
{"event":"sync","data":"QgG3qWH1RH<snip>8BfwB/ggE="}
...
Until I decide to hang up the connection with Ctrl-C. And this works perfectly fine with HTTP1.1 too:
$ curl -k -i -X PUT https://localhost:8080/example -d '{"event":"sync","data":"QgAAAQAAAA=="}' -H 'Content-Type: application/x-ndjson' --http1.1
< HTTP/1.1 200 OK
< Content-Type: application/x-ndjson; charset=utf-8
< Date: Sat, 26 Oct 2024 11:32:52 GMT
< Transfer-Encoding: chunked
{"event":"sync","data":"QgFbkqa2LT<snip>9CqZQD6wA="}
{"event":"sync","data":"QgEVJTDZHR<snip>F/AH+BAQ=="}
{"event":"sync","data":"QgG3qWH1RH<snip>8BfwB/ggE="}
...
The examples include an HTTP2 client, so the server has a self-signed certificate so that it can present HTTP2 over HTTPS. The client can still use HTTP1.1 as seen in the curl example above or the http1follower example.
This is purposefully built with only the Go standard library + github.com/automerge/automerge-go
. This is to reduce maintenance burden for me.
Unit tests, including Server and Client syncing, are executed through either make test
or the Github Actions CI.