Collaborative text over Braid-HTTP
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!
Demo: a Wiki!
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.
General Use as Server
Install it in your project:
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) })
Server API
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
keywithin 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.put_cb: [optional] Callback invoked after a PUT changes a resource. Signature:(key, val, {old_val, patches, version, parents})where:key- The resource keyval- The new document text after the PUTold_val- The document text before the PUTpatches- Array of patches applied (each{unit, range, content}), ornullfor full-body replacementsversion- The version after the PUTparents- The version prior to the PUT
- This is the main method of this library, and does all the work to handle Braid-HTTP
GETandPUTrequests 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 callscbwith the initial state and each update. The functioncbwill 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 thatPUTs them, for anyPUTsetting the samepeerheader.
- 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 togetsubscriptions that use this samepeerheader.
General Use as Client
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@~0.3/client/simpleton-sync.js"></script> <!-- 3. Wire it up --> <script> // Connect to server var simpleton = simpleton_client('https://braid.org/public-sandbox', { 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 some text in a box if you run this, and if you run it in another tab, you should be able to edit that text collaboratively.
How It Works
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_statefunction 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.
Advanced Integration
For better performance and control, you can work with patches instead of full text:
Patch Format
Each patch is an object with two properties:
range:[start, end]- The range of characters to deletecontent: The text to insert at that position
Patches in an array each have positions which refer to the original text before any other patches are applied.
Receiving Patches
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.
Custom Patch Generation
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.
Adding Multiplayer Cursor + Selections
This will render each peer's cursor and selection with colored highlights. Just add three lines to your simpleton client:
<!-- 1. Include these additional script tags --> <script src="https://unpkg.com/braid-text@~0.3/client/textarea-highlights.js"></script> <script src="https://unpkg.com/braid-text@~0.3/client/cursor-sync.js"></script> <!-- 2. Add two lines to your simpleton_client() call --> <script> var cursors = cursor_highlights(my_textarea, location.pathname) var simpleton = simpleton_client(location.pathname, { on_patches: (patches) => { apply_patches_and_update_selection(my_textarea, patches) cursors.on_patches(patches) // <-- update remote cursors }, get_state: () => my_textarea.value }) my_textarea.oninput = () => { cursors.on_edit(simpleton.changed()) // <-- send local cursor } </script>
Client API
Constructor
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:
Required Options
get_state: [required] Function that returns the current text state() => current_text_string
Incoming Updates (choose one)
-
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.
Outgoing Updates
-
get_patches: [optional] Custom function to generate patches(previous_state) => array_of_patches
If not provided, uses a simple prefix/suffix diff algorithm.
Note: All patches must reference positions in the original text before any patches are applied.
Additional Options
content_type: [optional] MIME type forAcceptandContent-Typeheaders
Methods
simpleton.changed(): Notify the client that local changes have occurred. Call this in your editor's change event handler. The client will callget_patchesandget_statewhen it's ready to send updates. Returns the array of JS-index patches (orundefinedif there was no change), which can be passed tocursors.on_edit().
Deprecated Options
The following options are deprecated and should be replaced with the new API:
→ Useapply_remote_updateon_patchesoron_stateinstead→ Usegenerate_local_diff_updateget_patchesandget_stateinstead
Multiplayer Cursor API
cursor_highlights(textarea, url) returns an object with:
cursors.on_patches(patches)— call after applying remote patches to transform and re-render remote cursorscursors.on_edit(patches)— call after local edits; pass the patches fromsimpleton.changed()to update cursor positions and broadcast your selectioncursors.destroy()— tear down listeners and DOM elements
Colors are auto-assigned per peer ID. See ?editor and ?markdown-editor in the demo server for working examples.
Testing
to run unit tests:
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.
to run fuzz tests:
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