braid-text 0.5.4 → 0.5.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-text",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "Library for collaborative text over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braid-text",
package/server-demo.js CHANGED
@@ -14,14 +14,15 @@ var server = require("http").createServer(async (req, res) => {
14
14
  if (req.method === 'OPTIONS') return res.end()
15
15
 
16
16
  var q = req.url.split('?').slice(-1)[0]
17
- if (q === 'editor' || q === 'markdown-editor') {
17
+ if (q === 'editor' || q === 'markdown-editor' || q === 'yjs-editor' || q === 'demo') {
18
18
  res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-cache" })
19
19
  require("fs").createReadStream(`./client/${q}.html`).pipe(res)
20
20
  return
21
21
  }
22
22
 
23
23
  if (req.url === '/simpleton-sync.js' || req.url === '/web-utils.js'
24
- || req.url === '/textarea-highlights.js' || req.url === '/cursor-sync.js') {
24
+ || req.url === '/textarea-highlights.js' || req.url === '/cursor-sync.js'
25
+ || req.url === '/yjs-sync.js') {
25
26
  res.writeHead(200, { "Content-Type": "text/javascript", "Cache-Control": "no-cache" })
26
27
  require("fs").createReadStream("./client" + req.url).pipe(res)
27
28
  return
package/server.js CHANGED
@@ -353,8 +353,7 @@ function create_braid_text() {
353
353
 
354
354
  var peer = req.headers['peer'],
355
355
  merge_type = req.headers['merge-type'] || 'simpleton'
356
- // TODO: accept 'yjs' merge_type here for Braid-HTTP yjs clients
357
- if (merge_type !== 'simpleton' && merge_type !== 'dt')
356
+ if (merge_type !== 'simpleton' && merge_type !== 'dt' && merge_type !== 'yjs')
358
357
  return my_end(400, `Unknown merge type: ${merge_type}`)
359
358
 
360
359
  var is_read = req.method === 'GET' || req.method === 'HEAD',
@@ -744,16 +743,14 @@ function create_braid_text() {
744
743
  if (getting.history === 'since-parents')
745
744
  throw new Error('yjs-text from arbitrary parents not yet implemented')
746
745
 
747
- var patches = braid_text.from_yjs_binary(
746
+ var yjs_updates = braid_text.from_yjs_binary(
748
747
  Y.encodeStateAsUpdate(resource.yjs.doc))
749
748
 
750
749
  if (!getting.subscribe)
751
- return { version: resource.version, parents: [], patches }
750
+ return yjs_updates
752
751
 
753
- if (patches.length)
754
- options.subscribe({
755
- version: resource.version, parents: [], patches
756
- })
752
+ for (var u of yjs_updates)
753
+ options.subscribe(u)
757
754
  }
758
755
 
759
756
  if (getting.subscribe) {
@@ -911,7 +908,10 @@ function create_braid_text() {
911
908
  ? options.yjs_update : new Uint8Array(options.yjs_update)
912
909
  } else if (patches && patches.length && patches[0].unit === 'yjs-text') {
913
910
  yjs_text_patches = patches
914
- yjs_binary = braid_text.to_yjs_binary(patches)
911
+ yjs_binary = braid_text.to_yjs_binary([{
912
+ version: options.version?.[0],
913
+ patches
914
+ }])
915
915
  }
916
916
 
917
917
  if (yjs_binary) {
@@ -986,18 +986,15 @@ function create_braid_text() {
986
986
 
987
987
  // Broadcast to yjs-text subscribers (skip sender)
988
988
  if (resource.yjs) {
989
- // If we received yjs-text patches, reuse them; otherwise
989
+ // If we received yjs-text updates, reuse them; otherwise
990
990
  // derive them from the binary update
991
- var yjs_patches = yjs_text_patches
992
- || braid_text.from_yjs_binary(yjs_binary)
993
- if (yjs_patches.length) {
991
+ var yjs_updates = yjs_text_patches
992
+ ? [{version: options.version, patches: yjs_text_patches}]
993
+ : braid_text.from_yjs_binary(yjs_binary)
994
+ for (var yjs_update of yjs_updates) {
994
995
  for (var client of resource.yjs.clients) {
995
- if (!peer || client.peer !== peer) {
996
- await client.send_update({
997
- version: resource.version,
998
- patches: yjs_patches
999
- })
1000
- }
996
+ if (!peer || client.peer !== peer)
997
+ await client.send_update(yjs_update)
1001
998
  }
1002
999
  }
1003
1000
  }
@@ -1233,15 +1230,12 @@ function create_braid_text() {
1233
1230
  // and we are mixing them. The update-level .version is the DT frontier.
1234
1231
  // Each patch's .version is a Yjs item ID (clientID-clock).
1235
1232
  if (resource.yjs && captured_yjs_update) {
1236
- var yjs_patches = braid_text.from_yjs_binary(captured_yjs_update)
1237
- for (var client of resource.yjs.clients) {
1238
- if (!peer || client.peer !== peer) {
1239
- await client.send_update({
1240
- version: resource.version, // DT version space
1241
- patches: yjs_patches // patches[].version: Yjs version space
1242
- })
1243
- }
1244
- }
1233
+ var yjs_updates = braid_text.from_yjs_binary(captured_yjs_update)
1234
+ if (braid_text.verbose) console.log('DT→Yjs broadcast:', yjs_updates.length, 'updates to', resource.yjs.clients.size, 'yjs clients')
1235
+ for (var yjs_update of yjs_updates)
1236
+ for (var client of resource.yjs.clients)
1237
+ if (!peer || client.peer !== peer)
1238
+ await client.send_update(yjs_update)
1245
1239
  }
1246
1240
 
1247
1241
  // Persist Yjs delta
@@ -3072,48 +3066,60 @@ function create_braid_text() {
3072
3066
  // Convert a Yjs binary update to yjs-text range patches.
3073
3067
  // Decodes the binary without needing a Y.Doc.
3074
3068
  // Returns array of {unit: 'yjs-text', range: '...', content: '...'}
3069
+ // Convert a Yjs binary update to an array of braid updates,
3070
+ // each with a version and patches in yjs-text format.
3075
3071
  braid_text.from_yjs_binary = function(update) {
3076
3072
  require_yjs()
3077
3073
  var decoded = Y.decodeUpdate(
3078
3074
  update instanceof Uint8Array ? update : new Uint8Array(update))
3079
- var patches = []
3075
+ var updates = []
3080
3076
 
3081
- // Convert inserted structs to yjs-text patches
3077
+ // Each inserted struct becomes one update with one insert patch.
3078
+ // GC'd structs (deleted items) have content.len but no content.str —
3079
+ // we emit placeholder text since the delete set will remove it anyway.
3082
3080
  for (var struct of decoded.structs) {
3083
- if (!struct.content?.str) continue // skip non-text items
3081
+ var text = struct.content?.str
3082
+ if (!text && struct.content?.len) text = '_'.repeat(struct.content.len)
3083
+ if (!text) continue // skip non-text items (e.g. format, embed)
3084
3084
  var id = struct.id
3085
3085
  var origin = struct.origin
3086
3086
  var rightOrigin = struct.rightOrigin
3087
3087
  var left = origin ? `${origin.client}-${origin.clock}` : ''
3088
3088
  var right = rightOrigin ? `${rightOrigin.client}-${rightOrigin.clock}` : ''
3089
- patches.push({
3090
- unit: 'yjs-text',
3091
- range: `(${left}:${right})`,
3092
- content: struct.content.str,
3093
- version: `${id.client}-${id.clock}`
3089
+ updates.push({
3090
+ version: [`${id.client}-${id.clock}`],
3091
+ patches: [{
3092
+ unit: 'yjs-text',
3093
+ range: `(${left}:${right})`,
3094
+ content: text,
3095
+ }]
3094
3096
  })
3095
3097
  }
3096
3098
 
3097
- // Convert delete set entries to yjs-text patches
3099
+ // Each delete range becomes one update with one delete patch
3098
3100
  for (var [clientID, deleteItems] of decoded.ds.clients) {
3099
3101
  for (var item of deleteItems) {
3100
3102
  var left = `${clientID}-${item.clock}`
3101
3103
  var right = `${clientID}-${item.clock + item.len - 1}`
3102
- patches.push({
3103
- unit: 'yjs-text',
3104
- range: `[${left}:${right}]`,
3105
- content: ''
3104
+ updates.push({
3105
+ version: [`${clientID}-${item.clock}`],
3106
+ patches: [{
3107
+ unit: 'yjs-text',
3108
+ range: `[${left}:${right}]`,
3109
+ content: ''
3110
+ }]
3106
3111
  })
3107
3112
  }
3108
3113
  }
3109
3114
 
3110
- return patches
3115
+ return updates
3111
3116
  }
3112
3117
 
3113
3118
  // Convert yjs-text range patches to a Yjs binary update.
3119
+ // Convert braid updates with yjs-text patches to a Yjs binary update.
3114
3120
  // This is the inverse of from_yjs_binary.
3115
- // Insert patches must have a .version field with the item ID as "client-clock".
3116
- braid_text.to_yjs_binary = function(patches) {
3121
+ // Accepts an array of updates, each with {version, patches}.
3122
+ braid_text.to_yjs_binary = function(updates) {
3117
3123
  require_yjs()
3118
3124
  var lib0_encoding = require('lib0/encoding')
3119
3125
  var encoder = new Y.UpdateEncoderV1()
@@ -3122,26 +3128,30 @@ function create_braid_text() {
3122
3128
  var inserts_by_client = new Map()
3123
3129
  var deletes_by_client = new Map()
3124
3130
 
3125
- for (var p of patches) {
3126
- var parsed = parse_yjs_range(p.range)
3127
- if (!parsed) throw new Error(`invalid yjs-text range: ${p.range}`)
3128
-
3129
- if (p.content.length > 0) {
3130
- // Insert — version is the item ID as "client-clock"
3131
- if (!p.version) throw new Error('insert patch requires .version = "client-clock"')
3132
- var v_parts = p.version.match(/^(\d+)-(\d+)$/)
3133
- if (!v_parts) throw new Error('invalid insert patch version: ' + p.version)
3134
- var item_id = { client: parseInt(v_parts[1]), clock: parseInt(v_parts[2]) }
3135
- var list = inserts_by_client.get(item_id.client) || []
3136
- list.push({ id: item_id, origin: parsed.left, rightOrigin: parsed.right, content: p.content })
3137
- inserts_by_client.set(item_id.client, list)
3138
- } else {
3139
- // Delete
3140
- if (!parsed.left) throw new Error('delete patch requires left ID')
3141
- var client = parsed.left.client
3142
- var list = deletes_by_client.get(client) || []
3143
- list.push({ clock: parsed.left.clock, len: parsed.right ? parsed.right.clock - parsed.left.clock + 1 : 1 })
3144
- deletes_by_client.set(client, list)
3131
+ for (var update of updates) {
3132
+ if (!update.patches) continue
3133
+ for (var p of update.patches) {
3134
+ var parsed = parse_yjs_range(p.range)
3135
+ if (!parsed) throw new Error(`invalid yjs-text range: ${p.range}`)
3136
+
3137
+ if (p.content.length > 0) {
3138
+ // Insert version on the update is the item ID as ["client-clock"]
3139
+ var v_str = Array.isArray(update.version) ? update.version[0] : update.version
3140
+ if (!v_str) throw new Error('insert update requires .version = ["client-clock"]')
3141
+ var v_parts = v_str.match(/^(\d+)-(\d+)$/)
3142
+ if (!v_parts) throw new Error('invalid update version: ' + v_str)
3143
+ var item_id = { client: parseInt(v_parts[1]), clock: parseInt(v_parts[2]) }
3144
+ var list = inserts_by_client.get(item_id.client) || []
3145
+ list.push({ id: item_id, origin: parsed.left, rightOrigin: parsed.right, content: p.content })
3146
+ inserts_by_client.set(item_id.client, list)
3147
+ } else {
3148
+ // Delete
3149
+ if (!parsed.left) throw new Error('delete patch requires left ID')
3150
+ var client = parsed.left.client
3151
+ var list = deletes_by_client.get(client) || []
3152
+ list.push({ clock: parsed.left.clock, len: parsed.right ? parsed.right.clock - parsed.left.clock + 1 : 1 })
3153
+ deletes_by_client.set(client, list)
3154
+ }
3145
3155
  }
3146
3156
  }
3147
3157