Skip to content

braid-org/braid-text

Repository files navigation

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
  • 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:

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 on Server

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)
})

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 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 to req.url.
  • This is the main method of this library, and does all the work to handle Braid-HTTP GET and PUT 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 calls cb with the initial state and each update. The function cb will be called with a Braid update of the form cb({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 that PUTs them, for any PUT setting the same peer 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 being PUT, 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 with hi. See Braid Range-Patches.
    • peer: [optional] Identifies this peer. This mutation will not be echoed back to get subscriptions that use this same peer header.

General Use on Client

<script src="https://unpkg.com/braid-text/simpleton-client.js"></script>
<script>

  // connect to the server
  let simpleton = simpleton_client('https://example.org/some-resource', {
    apply_remote_update: ({ state, patches }) => {

      // Apply the incoming state or patches to local text here.
      //
      // Example data:
      //   state: "Hello World"                               // The new text
      //   patches: [{ range: [5, 5], content: " World" }]    // Patches that create the new text
      //
      // Then return the new state of textarea as a string:
      return new_state
    },
    generate_local_diff_update: (prev_state) => {

      // Compute diff between prev_state ^ and the current textarea string, such as:
      //
      //   var patches = [{
      //     range: [5, 5],      // The range from position 5 to position 5
      //     content: " World"   // is replaced with the string " World"
      //   }]
      //
      // ...to insert something after a prev_state of "Hello".

      // Then return the new state (as a string) and the diff (as `patches`)
      return {new_state, patches}
    },
  })
    
  ...
    
  // When changes occur in client's textarea, let simpleton know,
  // so that it can call generate_local_diff_update() to ask for them.
  simpleton.changed()

</script>

See editor.html for a simple working example.

Client API

simpleton = simpleton_client(url, options)
  • url: The URL of the resource to synchronize with.

  • options: An object containing the following properties:

    Incoming Updates

    • on_patches: [optional] A function called when patches are received from the server:

      (patches) => {...}
      • patches: An array of patch objects, each representing a string-replace operation. Each patch object has:
        • range: An array of two numbers, [start, end], specifying the start and end positions of the characters to be deleted.
        • content: The text to be inserted in place of the deleted characters.

      Note that patches will always be in order, but the range positions of each patch reference the original string, i.e., the second patch's range values do not take into account the application of the first patch.

    • on_state: [optional] A function called when a complete state update is received from the server:

      (state) => {...}
      • state: The new complete value of the text.

    Local State Management

    • get_state: [required] A function that returns the current state of the text:

      () => current_state
    • get_patches: [optional] A function that generates patches representing changes between a previous state and the current state:

      (prev_state) => patches

      Returns an array of patch objects representing the changes. The default implementation finds a common prefix and suffix for a simple diff, but you can provide a more sophisticated implementation or track patches directly from your editor.

    Other Options

    • content_type: [optional] If set, this value will be sent in the Accept and Content-Type headers to the server.

Methods

  • simpleton.changed(): Call this function to report local updates whenever they occur, e.g., in the oninput event handler of a textarea being synchronized. The system will call get_patches when it needs to send updates to the server.

Deprecated Options

The following options are deprecated and should be replaced with the new API:

  • apply_remote_update → Use on_patches and on_state instead
  • generate_local_diff_update → Use get_patches and get_state instead

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

About

Text Synchronization libraries over Braid-HTTP

Resources

Stars

Watchers

Forks

Contributors 2

  •  
  •