braid-text 0.2.49 → 0.2.51
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 +71 -46
- package/index.js +19 -8
- package/package.json +1 -1
- package/test/test.html +84 -0
package/README.md
CHANGED
|
@@ -102,7 +102,7 @@ Here's a basic running example to start:
|
|
|
102
102
|
<!-- 1. Your textarea -->
|
|
103
103
|
<textarea id="my_textarea"></textarea>
|
|
104
104
|
|
|
105
|
-
<!-- 2. Include the
|
|
105
|
+
<!-- 2. Include the libraries -->
|
|
106
106
|
<script src="https://unpkg.com/braid-http@~1.3/braid-http-client.js"></script>
|
|
107
107
|
<script src="https://unpkg.com/braid-text/simpleton-client.js"></script>
|
|
108
108
|
|
|
@@ -119,88 +119,113 @@ Here's a basic running example to start:
|
|
|
119
119
|
</script>
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
-
You should see something if you run this (though the server in
|
|
122
|
+
You should see something if you run this (though the server in this example will likely ignore your changes).
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
### How It Works
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
The client uses a **decoupled update mechanism** for efficiency:
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
1. When users type, you call `simpleton.changed()` to notify the client that something changed
|
|
129
|
+
2. The client decides *when* to actually fetch and send updates based on network conditions
|
|
130
|
+
3. When ready, it calls your `get_state` function to get the current text
|
|
129
131
|
|
|
130
|
-
|
|
132
|
+
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.
|
|
131
133
|
|
|
132
|
-
|
|
134
|
+
### Advanced Integration
|
|
133
135
|
|
|
134
|
-
|
|
136
|
+
For better performance and control, you can work with patches instead of full text:
|
|
135
137
|
|
|
136
|
-
|
|
138
|
+
#### Receiving Patches
|
|
137
139
|
|
|
138
|
-
|
|
140
|
+
Instead of receiving complete text updates, you can process individual changes:
|
|
139
141
|
|
|
140
|
-
|
|
142
|
+
```javascript
|
|
143
|
+
var simpleton = simpleton_client(url, {
|
|
144
|
+
on_patches: (patches) => {
|
|
145
|
+
// Apply each patch to your editor..
|
|
146
|
+
},
|
|
147
|
+
get_state: () => editor.getValue()
|
|
148
|
+
})
|
|
149
|
+
```
|
|
141
150
|
|
|
142
|
-
|
|
151
|
+
This is more efficient for large documents and helps preserve cursor position.
|
|
143
152
|
|
|
144
|
-
|
|
153
|
+
#### Custom Patch Generation
|
|
154
|
+
|
|
155
|
+
You can provide your own diff algorithm or use patches from your editor's API:
|
|
145
156
|
|
|
146
157
|
```javascript
|
|
147
|
-
simpleton = simpleton_client(url,
|
|
158
|
+
var simpleton = simpleton_client(url, {
|
|
159
|
+
on_state: state => editor.setValue(state),
|
|
160
|
+
get_state: () => editor.getValue(),
|
|
161
|
+
get_patches: (prev_state) => {
|
|
162
|
+
// Use your own diff algorithm or editor's change tracking
|
|
163
|
+
return compute_patches(prev_state, editor.getValue())
|
|
164
|
+
}
|
|
165
|
+
})
|
|
148
166
|
```
|
|
149
167
|
|
|
150
|
-
-
|
|
151
|
-
- `options`: An object containing the following properties:
|
|
168
|
+
See [editor.html](https://github.com/braid-org/braid-text/blob/master/editor.html) for a complete example with CodeMirror integration.
|
|
152
169
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
- `on_patches`: <small style="color:lightgrey">[optional]</small> A function called when patches are received from the server:
|
|
156
|
-
|
|
157
|
-
```javascript
|
|
158
|
-
(patches) => {...}
|
|
159
|
-
```
|
|
170
|
+
## Client API
|
|
160
171
|
|
|
161
|
-
|
|
162
|
-
- `range`: An array of two numbers, `[start, end]`, specifying the start and end positions of the characters to be deleted.
|
|
163
|
-
- `content`: The text to be inserted in place of the deleted characters.
|
|
172
|
+
### Constructor
|
|
164
173
|
|
|
165
|
-
|
|
174
|
+
```javascript
|
|
175
|
+
simpleton = simpleton_client(url, options)
|
|
176
|
+
```
|
|
166
177
|
|
|
167
|
-
|
|
178
|
+
Creates a new Simpleton client that synchronizes with a Braid-Text server.
|
|
168
179
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
180
|
+
**Parameters:**
|
|
181
|
+
- `url`: The URL of the resource to synchronize with
|
|
182
|
+
- `options`: Configuration object with the following properties:
|
|
172
183
|
|
|
173
|
-
|
|
184
|
+
#### Required Options
|
|
174
185
|
|
|
175
|
-
|
|
186
|
+
- `get_state`: **[required]** Function that returns the current text state
|
|
187
|
+
```javascript
|
|
188
|
+
() => current_text_string
|
|
189
|
+
```
|
|
176
190
|
|
|
177
|
-
|
|
191
|
+
#### Incoming Updates (choose one)
|
|
178
192
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
193
|
+
- `on_state`: <small style="color:lightgrey">[optional]</small> Callback for receiving complete state updates
|
|
194
|
+
```javascript
|
|
195
|
+
(state) => { /* update your UI with new text */ }
|
|
196
|
+
```
|
|
182
197
|
|
|
183
|
-
|
|
198
|
+
- `on_patches`: <small style="color:lightgrey">[optional]</small> Callback for receiving incremental changes
|
|
199
|
+
```javascript
|
|
200
|
+
(patches) => { /* apply patches to your editor */ }
|
|
201
|
+
```
|
|
202
|
+
Each patch has:
|
|
203
|
+
- `range`: `[start, end]` - positions to delete (in original text coordinates)
|
|
204
|
+
- `content`: Text to insert at that position
|
|
205
|
+
|
|
206
|
+
**Note:** All patches reference positions in the original text before any patches are applied.
|
|
184
207
|
|
|
185
|
-
|
|
186
|
-
(prev_state) => patches
|
|
187
|
-
```
|
|
208
|
+
#### Outgoing Updates
|
|
188
209
|
|
|
189
|
-
|
|
210
|
+
- `get_patches`: <small style="color:lightgrey">[optional]</small> Custom function to generate patches
|
|
211
|
+
```javascript
|
|
212
|
+
(previous_state) => array_of_patches
|
|
213
|
+
```
|
|
214
|
+
If not provided, uses a simple prefix/suffix diff algorithm.
|
|
190
215
|
|
|
191
|
-
|
|
216
|
+
#### Additional Options
|
|
192
217
|
|
|
193
|
-
|
|
218
|
+
- `content_type`: <small style="color:lightgrey">[optional]</small> MIME type for `Accept` and `Content-Type` headers
|
|
194
219
|
|
|
195
220
|
### Methods
|
|
196
221
|
|
|
197
|
-
- `simpleton.changed()`:
|
|
222
|
+
- `simpleton.changed()`: Notify the client that local changes have occurred. Call this in your editor's change event handler. The client will call `get_patches` and `get_state` when it's ready to send updates.
|
|
198
223
|
|
|
199
224
|
### Deprecated Options
|
|
200
225
|
|
|
201
226
|
The following options are deprecated and should be replaced with the new API:
|
|
202
227
|
|
|
203
|
-
- ~~`apply_remote_update`~~ → Use `on_patches`
|
|
228
|
+
- ~~`apply_remote_update`~~ → Use `on_patches` or `on_state` instead
|
|
204
229
|
- ~~`generate_local_diff_update`~~ → Use `get_patches` and `get_state` instead
|
|
205
230
|
|
|
206
231
|
## Testing
|
package/index.js
CHANGED
|
@@ -70,8 +70,7 @@ braid_text.serve = async (req, res, options = {}) => {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
var get_current_version = () => ascii_ify(
|
|
73
|
-
resource.
|
|
74
|
-
.map(x => JSON.stringify(x)).join(", "))
|
|
73
|
+
resource.version.map(x => JSON.stringify(x)).join(", "))
|
|
75
74
|
|
|
76
75
|
if (req.method == "GET" || req.method == "HEAD") {
|
|
77
76
|
// make sure we have the necessary version and parents
|
|
@@ -133,7 +132,17 @@ braid_text.serve = async (req, res, options = {}) => {
|
|
|
133
132
|
accept_encoding:
|
|
134
133
|
req.headers['x-accept-encoding'] ??
|
|
135
134
|
req.headers['accept-encoding'],
|
|
136
|
-
subscribe: x =>
|
|
135
|
+
subscribe: x => {
|
|
136
|
+
|
|
137
|
+
// this is a sanity/rhobustness check..
|
|
138
|
+
// ..this digest is checked on the client..
|
|
139
|
+
// ..it is not strictly necessary
|
|
140
|
+
if (x.version && v_eq(x.version, resource.version)) {
|
|
141
|
+
x["Repr-Digest"] = `sha-256=:${require('crypto').createHash('sha256').update(Buffer.from(resource.val, "utf8")).digest('base64')}:`
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
res.sendVersion(x)
|
|
145
|
+
},
|
|
137
146
|
write: (x) => res.write(x)
|
|
138
147
|
}
|
|
139
148
|
|
|
@@ -233,7 +242,7 @@ braid_text.get = async (key, options) => {
|
|
|
233
242
|
if (options.parents) validate_version_array(options.parents)
|
|
234
243
|
|
|
235
244
|
let resource = (typeof key == 'string') ? await get_resource(key) : key
|
|
236
|
-
var version = resource.
|
|
245
|
+
var version = resource.version
|
|
237
246
|
|
|
238
247
|
if (!options.subscribe) {
|
|
239
248
|
if (options.transfer_encoding === 'dt') {
|
|
@@ -390,7 +399,7 @@ braid_text.put = async (key, options) => {
|
|
|
390
399
|
}
|
|
391
400
|
}
|
|
392
401
|
|
|
393
|
-
let parents = resource.
|
|
402
|
+
let parents = resource.version
|
|
394
403
|
let og_parents = options_parents || parents
|
|
395
404
|
|
|
396
405
|
let max_pos = resource.length_cache.get('' + og_parents) ??
|
|
@@ -526,6 +535,7 @@ braid_text.put = async (key, options) => {
|
|
|
526
535
|
|
|
527
536
|
for (let b of bytes) resource.doc.mergeBytes(b)
|
|
528
537
|
resource.val = resource.doc.get()
|
|
538
|
+
resource.version = resource.doc.getRemoteVersion().map(x => x.join("-")).sort()
|
|
529
539
|
|
|
530
540
|
var post_commit_updates = []
|
|
531
541
|
|
|
@@ -533,7 +543,7 @@ braid_text.put = async (key, options) => {
|
|
|
533
543
|
patches = get_xf_patches(resource.doc, v_before)
|
|
534
544
|
if (braid_text.verbose) console.log(JSON.stringify({ patches }))
|
|
535
545
|
|
|
536
|
-
let version = resource.
|
|
546
|
+
let version = resource.version
|
|
537
547
|
|
|
538
548
|
for (let client of resource.simpleton_clients) {
|
|
539
549
|
if (peer && client.peer === peer) {
|
|
@@ -546,7 +556,7 @@ braid_text.put = async (key, options) => {
|
|
|
546
556
|
// if the doc has been freed, exit early
|
|
547
557
|
if (resource.doc.__wbg_ptr === 0) return
|
|
548
558
|
|
|
549
|
-
let version = resource.
|
|
559
|
+
let version = resource.version
|
|
550
560
|
let x = { version }
|
|
551
561
|
x.parents = client.my_last_seen_version
|
|
552
562
|
|
|
@@ -609,7 +619,7 @@ braid_text.put = async (key, options) => {
|
|
|
609
619
|
}
|
|
610
620
|
} else {
|
|
611
621
|
if (resource.simpleton_clients.size) {
|
|
612
|
-
let version = resource.
|
|
622
|
+
let version = resource.version
|
|
613
623
|
patches = get_xf_patches(resource.doc, v_before)
|
|
614
624
|
let x = { version, parents, patches }
|
|
615
625
|
if (braid_text.verbose) console.log(`sending: ${JSON.stringify(x)}`)
|
|
@@ -681,6 +691,7 @@ async function get_resource(key) {
|
|
|
681
691
|
})
|
|
682
692
|
|
|
683
693
|
resource.val = resource.doc.get()
|
|
694
|
+
resource.version = resource.doc.getRemoteVersion().map(x => x.join("-")).sort()
|
|
684
695
|
|
|
685
696
|
resource.length_cache = createSimpleCache(braid_text.length_cache_size)
|
|
686
697
|
|
package/package.json
CHANGED
package/test/test.html
CHANGED
|
@@ -96,6 +96,90 @@ async function runTest(testName, testFunction, expectedResult) {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
runTest(
|
|
100
|
+
"test subscribing and verifying digests [simpleton]",
|
|
101
|
+
async () => {
|
|
102
|
+
let key = 'test-' + Math.random().toString(36).slice(2)
|
|
103
|
+
|
|
104
|
+
let r = await braid_fetch(`/${key}`, {
|
|
105
|
+
method: 'PUT',
|
|
106
|
+
version: ['hi-1'],
|
|
107
|
+
parents: [],
|
|
108
|
+
body: 'xx'
|
|
109
|
+
})
|
|
110
|
+
if (!r.ok) throw 'got: ' + r.statusCode
|
|
111
|
+
|
|
112
|
+
let r2 = await braid_fetch(`/${key}`, {
|
|
113
|
+
version: ['hi-0'],
|
|
114
|
+
subscribe: true
|
|
115
|
+
})
|
|
116
|
+
var parts = []
|
|
117
|
+
var p = new Promise(async (done, fail) => {
|
|
118
|
+
r2.subscribe(update => {
|
|
119
|
+
parts.push(update.extra_headers['repr-digest'])
|
|
120
|
+
if (parts.length > 1) done()
|
|
121
|
+
}, fail)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
await new Promise(done => setTimeout(done, 300))
|
|
125
|
+
let rr = await braid_fetch(`/${key}`, {
|
|
126
|
+
method: 'PUT',
|
|
127
|
+
version: ['hi-2'],
|
|
128
|
+
parents: ['hi-1'],
|
|
129
|
+
patches: [{unit: "text", range: "[1:1]", content: "Y"}]
|
|
130
|
+
})
|
|
131
|
+
if (!rr.ok) throw 'got: ' + rr.statusCode
|
|
132
|
+
|
|
133
|
+
await p
|
|
134
|
+
return JSON.stringify(parts)
|
|
135
|
+
},
|
|
136
|
+
'["sha-256=:Xd6JaIf2dUybFb/jpEGuSAbfL96UABMR4IvxEGIuC74=:","sha-256=:77cl3INcGEtczN0zK3eOgW/YWYAOm8ub73LkVcF2/rA=:"]'
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
runTest(
|
|
140
|
+
"test subscribing and verifying digests [dt]",
|
|
141
|
+
async () => {
|
|
142
|
+
let key = 'test-' + Math.random().toString(36).slice(2)
|
|
143
|
+
|
|
144
|
+
let r = await braid_fetch(`/${key}`, {
|
|
145
|
+
method: 'PUT',
|
|
146
|
+
version: ['hi-1'],
|
|
147
|
+
parents: [],
|
|
148
|
+
body: 'xx'
|
|
149
|
+
})
|
|
150
|
+
if (!r.ok) throw 'got: ' + r.statusCode
|
|
151
|
+
|
|
152
|
+
let r2 = await braid_fetch(`/${key}`, {
|
|
153
|
+
version: ['hi-0'],
|
|
154
|
+
headers: { 'merge-type': 'dt' },
|
|
155
|
+
subscribe: true
|
|
156
|
+
})
|
|
157
|
+
var parts = []
|
|
158
|
+
var p = new Promise(async (done, fail) => {
|
|
159
|
+
r2.subscribe(update => {
|
|
160
|
+
|
|
161
|
+
console.log(`update: ${JSON.stringify(update, null, 4)}`)
|
|
162
|
+
|
|
163
|
+
parts.push(update.extra_headers['repr-digest'])
|
|
164
|
+
if (parts.length > 1) done()
|
|
165
|
+
}, fail)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
await new Promise(done => setTimeout(done, 300))
|
|
169
|
+
let rr = await braid_fetch(`/${key}`, {
|
|
170
|
+
method: 'PUT',
|
|
171
|
+
version: ['hi-2'],
|
|
172
|
+
parents: ['hi-1'],
|
|
173
|
+
patches: [{unit: "text", range: "[2:2]", content: "Y"}]
|
|
174
|
+
})
|
|
175
|
+
if (!rr.ok) throw 'got: ' + rr.statusCode
|
|
176
|
+
|
|
177
|
+
await p
|
|
178
|
+
return JSON.stringify(parts)
|
|
179
|
+
},
|
|
180
|
+
'["sha-256=:Xd6JaIf2dUybFb/jpEGuSAbfL96UABMR4IvxEGIuC74=:","sha-256=:QknHazou37wCCwv3JXnCoAvXcKszP6xBTxLIiUAETgI=:"]'
|
|
181
|
+
)
|
|
182
|
+
|
|
99
183
|
runTest(
|
|
100
184
|
"test PUTing a version that the server already has",
|
|
101
185
|
async () => {
|