braid-text 0.2.47 → 0.2.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,7 +37,7 @@ Or try opening the URL in [Braid-Chrome](https://github.com/braid-org/braid-chro
37
37
 
38
38
  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.
39
39
 
40
- ## General Use on Server
40
+ ## General Use as Server
41
41
 
42
42
  Install it in your project:
43
43
  ```shell
@@ -94,51 +94,52 @@ http_server.on("request", (req, res) => {
94
94
  - `patches`: <small style="color:lightgrey">[optional]</small> 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](https://github.com/braid-org/braid-spec/blob/master/draft-toomim-httpbis-range-patch-01.txt).
95
95
  - `peer`: <small style="color:lightgrey">[optional]</small> Identifies this peer. This mutation will not be echoed back to `get` subscriptions that use this same `peer` header.
96
96
 
97
- ## General Use on Client
97
+ ## General Use as Client
98
+
99
+ Here's a basic running example to start:
98
100
 
99
101
  ```html
102
+ <!-- 1. Your textarea -->
103
+ <textarea id="my_textarea"></textarea>
104
+
105
+ <!-- 2. Include the library -->
106
+ <script src="https://unpkg.com/braid-http@~1.3/braid-http-client.js"></script>
100
107
  <script src="https://unpkg.com/braid-text/simpleton-client.js"></script>
101
- <script>
102
108
 
103
- // connect to the server
104
- let simpleton = simpleton_client('https://example.org/some-resource', {
105
- apply_remote_update: ({ state, patches }) => {
106
-
107
- // Apply the incoming state or patches to local text here.
108
- //
109
- // Example data:
110
- // state: "Hello World" // The new text
111
- // patches: [{ range: [5, 5], content: " World" }] // Patches that create the new text
112
- //
113
- // Then return the new state of textarea as a string:
114
- return new_state
115
- },
116
- generate_local_diff_update: (prev_state) => {
117
-
118
- // Compute diff between prev_state ^ and the current textarea string, such as:
119
- //
120
- // var patches = [{
121
- // range: [5, 5], // The range from position 5 to position 5
122
- // content: " World" // is replaced with the string " World"
123
- // }]
124
- //
125
- // ...to insert something after a prev_state of "Hello".
126
-
127
- // Then return the new state (as a string) and the diff (as `patches`)
128
- return {new_state, patches}
129
- },
109
+ <!-- 3. Wire it up -->
110
+ <script>
111
+ // Connect to server
112
+ var simpleton = simpleton_client('https://braid.org/simpleton_example', {
113
+ on_state: state => my_textarea.value = state, // incoming changes
114
+ get_state: () => my_textarea.value // outgoing changes
130
115
  })
131
-
132
- ...
133
-
134
- // When changes occur in client's textarea, let simpleton know,
135
- // so that it can call generate_local_diff_update() to ask for them.
136
- simpleton.changed()
137
116
 
117
+ // Tell simpleton when user types
118
+ my_textarea.oninput = () => simpleton.changed()
138
119
  </script>
139
120
  ```
140
121
 
141
- See [editor.html](https://raw.githubusercontent.com/braid-org/braid-text/master/editor.html) for a simple working example.
122
+ You should see something if you run this (though the server in that example will likely ignore your changes).
123
+
124
+ The basic idea is that you create a simpleton_client and tell it to connect to a url. Any incoming changes trigger the `on_state` callback. For outgoing changes, you'd think we would tell simpleton the new state when we get the `oninput` event — however, the network may be backed up, and simpleton wants to hold off on sending more updates. So instead, we just inform simpleton that a change has occurred and let it handle when to actually send the change to the server.
125
+
126
+ This decoupling between `simpleton.changed` and `get_state` is especially helpful when disconnecting from the server for extended periods (like getting on a plane for a couple hours). Without it, simpleton would have queued up a message for every change made during those two hours and try to send them all upon reconnection. With the decoupling, simpleton can wait until reconnection and then request a single diff to the new final state.
127
+
128
+ ### Finer-Grained Integration
129
+
130
+ There are a couple ways to do a more fine-grained integration:
131
+
132
+ **1. Receiving patches instead of full state**
133
+
134
+ Rather than receiving a completely new state in `on_state`, you can receive just changes to the current state in `on_patches`, as an array of patches. This can be more efficient, especially if you're integrating with an editor that has an API for making edits (rather than needing to set the whole string, as with an HTML textarea). Even with a textarea though, getting patches can be helpful for updating the cursor or selection position.
135
+
136
+ **2. Supplying custom patch generation**
137
+
138
+ Rather than only supplying `get_state`, you can also supply `get_patches` — a function that accepts a string representing the document at some point in the past and returns an array of patches to get from there to the current state.
139
+
140
+ Simpleton has a default simple way of computing this (scanning for a common prefix and suffix), but you may have a better function that handles complex cases. For example, when a user pastes in what seems like a large change that replaces the entire document, proper diff analysis might reveal it's just a few edits here and there. Also, with some editors, the analogous `oninput` event may provide patches for free, making `get_patches` more efficient.
141
+
142
+ You can see an example of both these finer-grained integration techniques here: [editor.html](https://raw.githubusercontent.com/braid-org/braid-text/master/editor.html).
142
143
 
143
144
  ## Client API
144
145
 
@@ -148,38 +149,59 @@ simpleton = simpleton_client(url, options)
148
149
 
149
150
  - `url`: The URL of the resource to synchronize with.
150
151
  - `options`: An object containing the following properties:
151
- - `apply_remote_update`: A function that will be called whenever an update is received from the server. It should have the following signature:
152
+
153
+ ### Incoming Updates
154
+
155
+ - `on_patches`: <small style="color:lightgrey">[optional]</small> A function called when patches are received from the server:
152
156
 
153
157
  ```javascript
154
- ({state, patches}) => {...}
158
+ (patches) => {...}
155
159
  ```
156
160
 
157
- - `state`: If present, represents the new value of the text.
158
- - `patches`: If present, an array of patch objects, each representing a string-replace operation. Each patch object has the following properties:
161
+ - `patches`: An array of patch objects, each representing a string-replace operation. Each patch object has:
159
162
  - `range`: An array of two numbers, `[start, end]`, specifying the start and end positions of the characters to be deleted.
160
163
  - `content`: The text to be inserted in place of the deleted characters.
161
164
 
162
165
  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.
163
166
 
164
- The function should apply the `state` or `patches` to the local text and return the new state.
167
+ - `on_state`: <small style="color:lightgrey">[optional]</small> A function called when a complete state update is received from the server:
168
+
169
+ ```javascript
170
+ (state) => {...}
171
+ ```
172
+
173
+ - `state`: The new complete value of the text.
174
+
175
+ ### Local State Management
165
176
 
166
- - `generate_local_diff_update`: A function that will be called whenever a local update occurs, but may be delayed if the network is congested. It should have the following signature:
177
+ - `get_state`: **[required]** A function that returns the current state of the text:
167
178
 
168
179
  ```javascript
169
- (prev_state) => {...}
180
+ () => current_state
170
181
  ```
171
182
 
172
- The function should calculate the difference between `prev_state` and the current state, and express this difference as an array of patches (similar to the ones described in `apply_remote_update`).
183
+ - `get_patches`: <small style="color:lightgrey">[optional]</small> A function that generates patches representing changes between a previous state and the current state:
173
184
 
174
- If a difference is detected, the function should return an object with the following properties:
175
- - `new_state`: The current state of the text.
176
- - `patches`: An array of patch objects representing the changes.
185
+ ```javascript
186
+ (prev_state) => patches
187
+ ```
177
188
 
178
- If no difference is detected, the function should return `undefined` or `null`.
189
+ 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.
190
+
191
+ ### Other Options
179
192
 
180
193
  - `content_type`: <small style="color:lightgrey">[optional]</small> If set, this value will be sent in the `Accept` and `Content-Type` headers to the server.
181
194
 
182
- - `simpleton.changed()`: Call this function to report local updates whenever they occur, e.g., in the `oninput` event handler of a textarea being synchronized.
195
+ ### Methods
196
+
197
+ - `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.
198
+
199
+ ### Deprecated Options
200
+
201
+ The following options are deprecated and should be replaced with the new API:
202
+
203
+ - ~~`apply_remote_update`~~ → Use `on_patches` and `on_state` instead
204
+ - ~~`generate_local_diff_update`~~ → Use `get_patches` and `get_state` instead
183
205
 
184
206
  ## Testing
185
207
 
package/editor.html CHANGED
@@ -9,16 +9,9 @@
9
9
  <script src="/simpleton-client.js"></script>
10
10
  <script>
11
11
  let simpleton = simpleton_client(location.pathname, {
12
- apply_remote_update: ({ state, patches }) => {
13
- if (state !== undefined) texty.value = state;
14
- else apply_patches_and_update_selection(texty, patches);
15
- return texty.value;
16
- },
17
- generate_local_diff_update: (prev_state) => {
18
- var patches = diff(prev_state, texty.value);
19
- if (patches.length === 0) return null;
20
- return { patches, new_state: texty.value };
21
- },
12
+ on_patches: (patches) => apply_patches_and_update_selection(texty, patches),
13
+ get_patches: (prev_state) => diff(prev_state, texty.value),
14
+ get_state: () => texty.value,
22
15
  on_error: (e) => {
23
16
  texty.disabled = true
24
17
  texty.style.background = '#fee'
@@ -27,27 +27,14 @@ t = function() {
27
27
  };
28
28
 
29
29
  var simpleton = simpleton_client(location.pathname, {
30
- apply_remote_update: function(x) {
31
- if (x.state !== void 0) {
32
- t().value = x.state;
33
- } else {
34
- apply_patches_and_update_selection(t(), x.patches);
35
- }
30
+ on_patches: (patches) => {
31
+ apply_patches_and_update_selection(t(), patches);
36
32
  state.source = t().value;
37
33
  update_markdown_later();
38
- return t().value;
39
- },
40
- generate_local_diff_update: function(prev_state) {
41
- var patches;
42
- patches = diff(prev_state, t().value);
43
- if (patches.length === 0) {
44
- return null;
45
- }
46
- return {
47
- patches: patches,
48
- new_state: t().value
49
- };
50
34
  },
35
+ get_patches: (prev_state) => diff(prev_state, t().value),
36
+ get_state: () => t().value,
37
+
51
38
  on_error: (e) => {
52
39
  t().disabled = true
53
40
  t().style.background = '#fee'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-text",
3
- "version": "0.2.47",
3
+ "version": "0.2.49",
4
4
  "description": "Library for collaborative text over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braid-text",
@@ -2,22 +2,51 @@
2
2
  //
3
3
  // url: resource endpoint
4
4
  //
5
- // apply_remote_update: ({patches, state}) => {...}
5
+ // on_patches?: (patches) => void
6
+ // processes incoming patches
7
+ //
8
+ // on_state?: (state) => void
9
+ // processes incoming state
10
+ //
11
+ // get_patches?: (prev_state) => patches
12
+ // returns patches representing diff
13
+ // between prev_state and current state,
14
+ // which are guaranteed to be different
15
+ // if this method is being called
16
+ // (the default does this in a fast/simple way,
17
+ // finding a common prefix and suffix,
18
+ // but you can supply something better,
19
+ // or possibly keep track of patches as they come from your editor)
20
+ //
21
+ // get_state: () => current_state
22
+ // returns the current state
23
+ //
24
+ // [DEPRECATED] apply_remote_update: ({patches, state}) => {...}
6
25
  // this is for incoming changes;
7
26
  // one of these will be non-null,
8
27
  // and can be applied to the current state.
9
28
  //
10
- // generate_local_diff_update: (prev_state) => {...}
29
+ // [DEPRECATED] generate_local_diff_update: (prev_state) => {...}
11
30
  // this is to generate outgoing changes,
12
31
  // and if there are changes, returns { patches, new_state }
13
32
  //
14
33
  // content_type: used for Accept and Content-Type headers
15
34
  //
16
- // returns { changed(): (diff_function) => {...} }
17
- // this is for outgoing changes;
18
- // diff_function = () => ({patches, new_version}).
35
+ // returns { changed }
36
+ // call changed whenever there is a local change,
37
+ // and the system will call get_patches when it needs to.
19
38
  //
20
- function simpleton_client(url, { apply_remote_update, generate_local_diff_update, content_type, on_error, on_res }) {
39
+ function simpleton_client(url, {
40
+ on_patches,
41
+ on_state,
42
+ get_patches,
43
+ get_state,
44
+ apply_remote_update, // DEPRECATED
45
+ generate_local_diff_update, // DEPRECATED
46
+ content_type,
47
+ on_error,
48
+ on_res
49
+ }) {
21
50
  var peer = Math.random().toString(36).substr(2)
22
51
  var current_version = []
23
52
  var prev_state = ""
@@ -69,7 +98,19 @@ function simpleton_client(url, { apply_remote_update, generate_local_diff_update
69
98
  }
70
99
  }
71
100
 
72
- prev_state = apply_remote_update(update)
101
+ if (apply_remote_update) {
102
+ // DEPRECATED
103
+ prev_state = apply_remote_update(update)
104
+ } else {
105
+ var patches = update.patches ||
106
+ [{range: [0, 0], content: update.state}]
107
+ if (on_patches) {
108
+ on_patches(patches)
109
+ prev_state = get_state()
110
+ } else prev_state = apply_patches(prev_state, patches)
111
+ }
112
+
113
+ if (on_state) on_state(prev_state)
73
114
  }
74
115
  }, on_error)
75
116
  }).catch(on_error)
@@ -81,9 +122,17 @@ function simpleton_client(url, { apply_remote_update, generate_local_diff_update
81
122
  changed: async () => {
82
123
  if (outstanding_changes >= max_outstanding_changes) return
83
124
  while (true) {
84
- var update = generate_local_diff_update(prev_state)
85
- if (!update) return // Stop if there wasn't a change!
86
- var {patches, new_state} = update
125
+ if (generate_local_diff_update) {
126
+ // DEPRECATED
127
+ var update = generate_local_diff_update(prev_state)
128
+ if (!update) return // Stop if there wasn't a change!
129
+ var {patches, new_state} = update
130
+ } else {
131
+ var new_state = get_state()
132
+ if (new_state === prev_state) return // Stop if there wasn't a change!
133
+ var patches = get_patches ? get_patches(prev_state) :
134
+ [simple_diff(prev_state, new_state)]
135
+ }
87
136
 
88
137
  // convert from js-indicies to code-points
89
138
  let c = 0
@@ -133,18 +182,42 @@ function simpleton_client(url, { apply_remote_update, generate_local_diff_update
133
182
  }
134
183
  }
135
184
  }
136
- }
137
185
 
138
- function get_char_size(s, i) {
139
- const charCode = s.charCodeAt(i)
140
- return (charCode >= 0xd800 && charCode <= 0xdbff) ? 2 : 1
141
- }
186
+ function get_char_size(s, i) {
187
+ const charCode = s.charCodeAt(i)
188
+ return (charCode >= 0xd800 && charCode <= 0xdbff) ? 2 : 1
189
+ }
190
+
191
+ function count_code_points(str) {
192
+ let code_points = 0
193
+ for (let i = 0; i < str.length; i++) {
194
+ if (str.charCodeAt(i) >= 0xd800 && str.charCodeAt(i) <= 0xdbff) i++
195
+ code_points++
196
+ }
197
+ return code_points
198
+ }
199
+
200
+ function simple_diff(a, b) {
201
+ // Find common prefix
202
+ var p = 0
203
+ var len = Math.min(a.length, b.length)
204
+ while (p < len && a[p] === b[p]) p++
142
205
 
143
- function count_code_points(str) {
144
- let code_points = 0
145
- for (let i = 0; i < str.length; i++) {
146
- if (str.charCodeAt(i) >= 0xd800 && str.charCodeAt(i) <= 0xdbff) i++
147
- code_points++
206
+ // Find common suffix (from what remains after prefix)
207
+ var s = 0
208
+ len -= p
209
+ while (s < len && a[a.length - s - 1] === b[b.length - s - 1]) s++
210
+
211
+ return {range: [p, a.length - s], content: b.slice(p, b.length - s)}
212
+ }
213
+
214
+ function apply_patches(state, patches) {
215
+ var offset = 0
216
+ for (var p of patches) {
217
+ state = state.substring(0, p.range[0] + offset) + p.content +
218
+ state.substring(p.range[1] + offset)
219
+ offset += p.content.length - (p.range[1] - p.range[0])
220
+ }
221
+ return state
148
222
  }
149
- return code_points
150
223
  }