braid-text 0.2.46 → 0.2.48
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 +34 -13
- package/editor.html +3 -10
- package/index.js +13 -7
- package/markdown-editor.html +5 -18
- package/package.json +1 -1
- package/simpleton-client.js +94 -21
- package/test/test.html +39 -0
package/README.md
CHANGED
|
@@ -148,38 +148,59 @@ simpleton = simpleton_client(url, options)
|
|
|
148
148
|
|
|
149
149
|
- `url`: The URL of the resource to synchronize with.
|
|
150
150
|
- `options`: An object containing the following properties:
|
|
151
|
-
|
|
151
|
+
|
|
152
|
+
### Incoming Updates
|
|
153
|
+
|
|
154
|
+
- `on_patches`: <small style="color:lightgrey">[optional]</small> A function called when patches are received from the server:
|
|
152
155
|
|
|
153
156
|
```javascript
|
|
154
|
-
(
|
|
157
|
+
(patches) => {...}
|
|
155
158
|
```
|
|
156
159
|
|
|
157
|
-
- `
|
|
158
|
-
- `patches`: If present, an array of patch objects, each representing a string-replace operation. Each patch object has the following properties:
|
|
160
|
+
- `patches`: An array of patch objects, each representing a string-replace operation. Each patch object has:
|
|
159
161
|
- `range`: An array of two numbers, `[start, end]`, specifying the start and end positions of the characters to be deleted.
|
|
160
162
|
- `content`: The text to be inserted in place of the deleted characters.
|
|
161
163
|
|
|
162
164
|
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
165
|
|
|
164
|
-
|
|
166
|
+
- `on_state`: <small style="color:lightgrey">[optional]</small> A function called when a complete state update is received from the server:
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
(state) => {...}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
- `state`: The new complete value of the text.
|
|
173
|
+
|
|
174
|
+
### Local State Management
|
|
165
175
|
|
|
166
|
-
- `
|
|
176
|
+
- `get_state`: **[required]** A function that returns the current state of the text:
|
|
167
177
|
|
|
168
178
|
```javascript
|
|
169
|
-
(
|
|
179
|
+
() => current_state
|
|
170
180
|
```
|
|
171
181
|
|
|
172
|
-
|
|
182
|
+
- `get_patches`: <small style="color:lightgrey">[optional]</small> A function that generates patches representing changes between a previous state and the current state:
|
|
173
183
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
184
|
+
```javascript
|
|
185
|
+
(prev_state) => patches
|
|
186
|
+
```
|
|
177
187
|
|
|
178
|
-
|
|
188
|
+
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.
|
|
189
|
+
|
|
190
|
+
### Other Options
|
|
179
191
|
|
|
180
192
|
- `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
193
|
|
|
182
|
-
|
|
194
|
+
### Methods
|
|
195
|
+
|
|
196
|
+
- `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.
|
|
197
|
+
|
|
198
|
+
### Deprecated Options
|
|
199
|
+
|
|
200
|
+
The following options are deprecated and should be replaced with the new API:
|
|
201
|
+
|
|
202
|
+
- ~~`apply_remote_update`~~ → Use `on_patches` and `on_state` instead
|
|
203
|
+
- ~~`generate_local_diff_update`~~ → Use `get_patches` and `get_state` instead
|
|
183
204
|
|
|
184
205
|
## Testing
|
|
185
206
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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'
|
package/index.js
CHANGED
|
@@ -293,14 +293,20 @@ braid_text.get = async (key, options) => {
|
|
|
293
293
|
} else {
|
|
294
294
|
|
|
295
295
|
if (options.accept_encoding?.match(/updates\s*\((.*)\)/)?.[1].split(',').map(x=>x.trim()).includes('dt')) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
doc.
|
|
296
|
+
// optimization: if client wants past current version,
|
|
297
|
+
// send empty dt
|
|
298
|
+
if (options.parents && v_eq(options.parents, version)) {
|
|
299
|
+
options.subscribe({ encoding: 'dt', body: new Doc().toBytes() })
|
|
300
|
+
} else {
|
|
301
|
+
var bytes = resource.doc.toBytes()
|
|
302
|
+
if (options.parents) {
|
|
303
|
+
var doc = Doc.fromBytes(bytes)
|
|
304
|
+
bytes = doc.getPatchSince(
|
|
305
|
+
dt_get_local_version(bytes, options.parents))
|
|
306
|
+
doc.free()
|
|
307
|
+
}
|
|
308
|
+
options.subscribe({ encoding: 'dt', body: bytes })
|
|
302
309
|
}
|
|
303
|
-
options.subscribe({ encoding: 'dt', body: bytes })
|
|
304
310
|
} else {
|
|
305
311
|
var updates = null
|
|
306
312
|
if (!options.parents && !options.version) {
|
package/markdown-editor.html
CHANGED
|
@@ -27,27 +27,14 @@ t = function() {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
var simpleton = simpleton_client(location.pathname, {
|
|
30
|
-
|
|
31
|
-
|
|
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
package/simpleton-client.js
CHANGED
|
@@ -2,22 +2,51 @@
|
|
|
2
2
|
//
|
|
3
3
|
// url: resource endpoint
|
|
4
4
|
//
|
|
5
|
-
//
|
|
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
|
|
17
|
-
//
|
|
18
|
-
//
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
}
|
package/test/test.html
CHANGED
|
@@ -674,6 +674,45 @@ runTest(
|
|
|
674
674
|
'xy'
|
|
675
675
|
)
|
|
676
676
|
|
|
677
|
+
runTest(
|
|
678
|
+
"test accept-encoding updates(dt) (with parents which are current version)",
|
|
679
|
+
async () => {
|
|
680
|
+
await dt_p
|
|
681
|
+
let key = 'test-' + Math.random().toString(36).slice(2)
|
|
682
|
+
var doc = new Doc('hi')
|
|
683
|
+
doc.ins(0, 'xy')
|
|
684
|
+
|
|
685
|
+
let r = await braid_fetch(`/${key}`, {
|
|
686
|
+
method: 'PUT',
|
|
687
|
+
version: ['hi-1'],
|
|
688
|
+
parents: [],
|
|
689
|
+
body: 'xy'
|
|
690
|
+
})
|
|
691
|
+
if (!r.ok) throw 'got: ' + r.statusCode
|
|
692
|
+
|
|
693
|
+
var a = new AbortController()
|
|
694
|
+
let r2 = await braid_fetch(`/${key}`, {
|
|
695
|
+
signal: a.signal,
|
|
696
|
+
parents: ['hi-1'],
|
|
697
|
+
subscribe: true,
|
|
698
|
+
headers: {
|
|
699
|
+
'merge-type': 'dt',
|
|
700
|
+
'X-Accept-Encoding': 'updates(dt)'
|
|
701
|
+
}
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
return await new Promise(done => {
|
|
705
|
+
r2.subscribe(u => {
|
|
706
|
+
doc.mergeBytes(u.body)
|
|
707
|
+
done(doc.get())
|
|
708
|
+
doc.free()
|
|
709
|
+
a.abort()
|
|
710
|
+
})
|
|
711
|
+
})
|
|
712
|
+
},
|
|
713
|
+
'xy'
|
|
714
|
+
)
|
|
715
|
+
|
|
677
716
|
runTest(
|
|
678
717
|
"test accept-encoding updates(dt)",
|
|
679
718
|
async () => {
|