This library provides a simple http route handler, along with client code, enabling fast text synchronization over a standard protocol.
- Supports Braid-HTTP protocol
- Supports Simpleton merge-type
- Enables light clients
- As little as 50 lines of code!
- With zero history overhead on client
- Supports backpressure to run smoothly on constrained servers
- Server merges with Diamond-Types
- Enables light clients
- Supports Diamond Types merge-type
- Fully peer-to-peer CRDT
- Fast / Robust / Extensively fuzz-tested
- Developed in braid.org
This library makes it safe, easy & efficient to add collaborative text editing to every user-editable string in your web app. Make your app multiplayer!
Check out the demo video 📺 from the Braid 86 release!
This will run a collaboratively-editable wiki:
npm install
node server-demo.js
Now open these URLs in your browser:
- http://localhost:8888/demo (to see the demo text)
- http://localhost:8888/demo?editor (to edit the text)
- http://localhost:8888/demo?markdown-editor (to edit it as markdown)
- http://localhost:8888/any-other-path?editor (to create a new page, just go to its URL, and then start editing)
Or try opening the URL in Braid-Chrome, or another Braid client, to edit it directly!
Check out the server-demo.js
file to see examples for how to add simple access control, where a user need only enter a password into a cookie in the javascript console like: document.cookie = 'password'
; and a /pages
endpoint to show all the edited pages.
Install it in your project:
npm install braid-text
Import the request handler into your code, and use it to handle HTTP requests wherever you want:
var braid_text = require("braid-text")
http_server.on("request", (req, res) => {
// Your server logic...
// Whenever desired, serve braid text for this request/response:
braid_text.serve(req, res)
})
braid_text.db_folder = './braid-text-db' // <-- this is the default
- This is where the Diamond-Types history files will be stored for each resource.
- This folder will be created if it doesn't exist.
- The files for a resource will all be prefixed with a url-encoding of
key
within this folder.
braid_text.serve(req, res, options)
req
: Incoming HTTP request object.res
: Outgoing HTTP response object.options
: [optional] An object containing additional options:key
: [optional] ID of text resource to sync with. Defaults toreq.url
.
- This is the main method of this library, and does all the work to handle Braid-HTTP
GET
andPUT
requests concerned with a specific text resource.
await braid_text.get(key)
key
: ID of text resource.- Returns the text of the resource as a string.
await braid_text.get(key, options)
key
: ID of text resource.options
: An object containing additional options, like http headers:version
: [optional] The version to get, as an array of strings. (The array is typically length 1.)parents
: [optional] The version to start the subscription at, as an array of strings.subscribe: cb
: [optional] Instead of returning the state; subscribes to the state, and callscb
with the initial state and each update. The functioncb
will be called with a Braid update of the formcb({version, parents, body, patches})
.merge_type
: [optional] The CRDT/OT merge-type algorithm to emulate. Currently supports"simpleton"
(default) and"dt"
.peer
: [optional] Unique string ID that identifies the peer making the subscription. Mutations will not be echoed back to the same peer thatPUT
s them, for anyPUT
setting the samepeer
header.
- If NOT subscribing, returns
{version: <current_version>, body: <current-text>}
. If subscribing, returns nothing.
await braid_text.put(key, options)
key
: ID of text resource.options
: An object containing additional options, like http headers:version
: [optional] The version beingPUT
, as an array of strings. Will be generated if not provided.parents
: [optional] The previous version being updated, as array of strings. Defaults to the server’s current version.body
: [optional] Use this to completely replace the existing text with this new text. See Braid updates.patches
: [optional] Array of patches, each of the form{unit: 'text', range: '[1:3]', content: 'hi'}
, which would replace the second and third unicode code-points in the text withhi
. See Braid Range-Patches.peer
: [optional] Identifies this peer. This mutation will not be echoed back toget
subscriptions that use this samepeer
header.
Here's a basic running example to start:
<!-- 1. Your textarea -->
<textarea id="my_textarea"></textarea>
<!-- 2. Include the libraries -->
<script src="https://unpkg.com/braid-http@~1.3/braid-http-client.js"></script>
<script src="https://unpkg.com/braid-text/simpleton-client.js"></script>
<!-- 3. Wire it up -->
<script>
// Connect to server
var simpleton = simpleton_client('https://braid.org/simpleton_example', {
on_state: state => my_textarea.value = state, // incoming changes
get_state: () => my_textarea.value // outgoing changes
})
// Tell simpleton when user types
my_textarea.oninput = () => simpleton.changed()
</script>
You should see something if you run this (though the server in this example will likely ignore your changes).
The client uses a decoupled update mechanism for efficiency:
- When users type, you call
simpleton.changed()
to notify the client that something changed - The client decides when to actually fetch and send updates based on network conditions
- When ready, it calls your
get_state
function to get the current text
This design prevents network congestion and handles disconnections gracefully. For example, if you edit offline for hours, the client will send just one efficient diff when reconnecting, rather than thousands of individual keystrokes.
For better performance and control, you can work with patches instead of full text:
Instead of receiving complete text updates, you can process individual changes:
var simpleton = simpleton_client(url, {
on_patches: (patches) => {
// Apply each patch to your editor..
},
get_state: () => editor.getValue()
})
This is more efficient for large documents and helps preserve cursor position.
You can provide your own diff algorithm or use patches from your editor's API:
var simpleton = simpleton_client(url, {
on_state: state => editor.setValue(state),
get_state: () => editor.getValue(),
get_patches: (prev_state) => {
// Use your own diff algorithm or editor's change tracking
return compute_patches(prev_state, editor.getValue())
}
})
See editor.html for a complete example.
simpleton = simpleton_client(url, options)
Creates a new Simpleton client that synchronizes with a Braid-Text server.
Parameters:
url
: The URL of the resource to synchronize withoptions
: Configuration object with the following properties:
get_state
: [required] Function that returns the current text state() => current_text_string
-
on_state
: [optional] Callback for receiving complete state updates(state) => { /* update your UI with new text */ }
-
on_patches
: [optional] Callback for receiving incremental changes(patches) => { /* apply patches to your editor */ }
Each patch has:
range
:[start, end]
- positions to delete (in original text coordinates)content
: Text to insert at that position
Note: All patches reference positions in the original text before any patches are applied.
get_patches
: [optional] Custom function to generate patchesIf not provided, uses a simple prefix/suffix diff algorithm.(previous_state) => array_of_patches
content_type
: [optional] MIME type forAccept
andContent-Type
headers
simpleton.changed()
: Notify the client that local changes have occurred. Call this in your editor's change event handler. The client will callget_patches
andget_state
when it's ready to send updates.
The following options are deprecated and should be replaced with the new API:
→ Useapply_remote_update
on_patches
oron_state
instead→ Usegenerate_local_diff_update
get_patches
andget_state
instead
first run the test server:
npm install
node test/server.js
then open http://localhost:8889/test.html, and the boxes should turn green as the tests pass.
npm install
node test/test.js
if the last output line looks like this, good:
t = 9999, seed = 1397019, best_n = Infinity @ NaN
but it's bad if it looks like this:
t = 9999, seed = 1397019, best_n = 5 @ 1396791
the number at the end is the random seed that generated the simplest error example