braid-text 0.5.15 → 0.5.17

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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/package.json +2 -2
  3. package/server.js +37 -8
package/README.md CHANGED
@@ -77,6 +77,7 @@ http_server.on("request", (req, res) => {
77
77
  - `version` - The version after the PUT
78
78
  - `parents` - The version prior to the PUT
79
79
  - 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.
80
+ - A `GET` with `Subscribe: true` and `Method-Override: HEAD` headers subscribes to header-only updates: each update carries just `Version` and `Parents` headers, with no body or patches — useful for cheaply knowing *that* something changed. (A `Method-Override: HEAD` on a `GET` without `Subscribe` acts like a normal `HEAD` request.)
80
81
 
81
82
  `await braid_text.get(key)`
82
83
  - `key`: ID of text resource.
@@ -86,10 +87,11 @@ http_server.on("request", (req, res) => {
86
87
  - `key`: ID of text resource.
87
88
  - `options`: An object containing additional options, like http headers:
88
89
  - `version`: <small style="color:lightgrey">[optional]</small> The [version](https://datatracker.ietf.org/doc/html/draft-toomim-httpbis-braid-http#section-2) to get, as an array of strings. (The array is typically length 1.)
89
- - `parents`: <small style="color:lightgrey">[optional]</small> The version to start the subscription at, as an array of strings.
90
+ - `parents`: <small style="color:lightgrey">[optional]</small> The version to start the subscription at, as an array of strings. If this is already the current version, no initial update is sent.
90
91
  - `subscribe: cb`: <small style="color:lightgrey">[optional]</small> Instead of returning the state; [subscribes](https://datatracker.ietf.org/doc/html/draft-toomim-httpbis-braid-http#section-4) to the state, and calls `cb` with the initial state and each update. The function `cb` will be called with a Braid [update](https://datatracker.ietf.org/doc/html/draft-toomim-httpbis-braid-http#section-3) of the form `cb({version, parents, body, patches})`.
91
92
  - `merge_type`: <small style="color:lightgrey">[optional]</small> The CRDT/OT [merge-type](https://raw.githubusercontent.com/braid-org/braid-spec/master/draft-toomim-httpbis-merge-types-00.txt) algorithm to emulate. Currently supports `"simpleton"` (default) and `"dt"`.
92
93
  - `peer`: <small style="color:lightgrey">[optional]</small> Unique string ID that identifies the peer making the subscription. Mutations will not be echoed back to the same peer that `PUT`s them, for any `PUT` setting the same `peer` header.
94
+ - `head`: <small style="color:lightgrey">[optional]</small> If true, the subscription receives header-only updates of the form `{version, parents}` — no `body` or `patches`. Useful for cheaply knowing *that* something changed, without the cost of materializing history or patch content. On subscribe, `cb` is called once with the current version (or not at all, if `parents` is already current — as with any subscription); after that, once per update. Only meaningful with `subscribe`.
93
95
  - If NOT subscribing, returns `{version: <current_version>, body: <current-text>}`. If subscribing, returns nothing.
94
96
 
95
97
  `await braid_text.put(key, options)`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-text",
3
- "version": "0.5.15",
3
+ "version": "0.5.17",
4
4
  "description": "Library for collaborative text over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braid-text",
@@ -27,7 +27,7 @@
27
27
  ],
28
28
  "dependencies": {
29
29
  "@braid.org/diamond-types-node": "^2.0.1",
30
- "braid-http": "~1.3.125"
30
+ "braid-http": "~1.3.131"
31
31
  },
32
32
  "devDependencies": {
33
33
  "yjs": "^13.6.0"
package/server.js CHANGED
@@ -372,9 +372,15 @@ function create_braid_text() {
372
372
  if (merge_type !== 'simpleton' && merge_type !== 'dt' && merge_type !== 'yjs')
373
373
  return my_end(400, `Unknown merge type: ${merge_type}`)
374
374
 
375
+ // Method-Override: HEAD makes a GET act like a HEAD request.
376
+ // Combined with Subscribe, it subscribes to header-only updates
377
+ // (Version/Parents, no body or patches) — which a real HEAD request
378
+ // can't do, since HEAD responses can't stream a body.
379
+ var head_override = /^head$/i.test(req.headers['method-override'] ?? '')
380
+
375
381
  var is_read = req.method === 'GET' || req.method === 'HEAD',
376
382
  is_write = req.method === 'PUT' || req.method === 'POST' || req.method === 'PATCH',
377
- is_head = req.method === 'HEAD'
383
+ is_head = req.method === 'HEAD' || (head_override && !req.subscribe)
378
384
 
379
385
  // ── Handle simple methods that don't need further processing ──
380
386
 
@@ -506,12 +512,16 @@ function create_braid_text() {
506
512
  version: req.version,
507
513
  parents: req.parents,
508
514
  merge_type,
515
+ head: head_override,
509
516
  signal: aborter.signal,
510
517
  accept_encoding:
511
518
  req.headers['x-accept-encoding'] ?? req.headers['accept-encoding'],
512
519
  subscribe: update => {
513
520
  // Add digest for integrity checking on the client
514
- if (update.version && v_eq(update.version, resource.version))
521
+ // (skipped for header-only updates, which carry
522
+ // no content to check)
523
+ if (!head_override
524
+ && update.version && v_eq(update.version, resource.version))
515
525
  update['Repr-Digest'] = get_digest(resource.val)
516
526
 
517
527
  // Collapse single-element patches array for HTTP
@@ -519,7 +529,7 @@ function create_braid_text() {
519
529
  update.patch = update.patches[0]
520
530
  delete update.patches
521
531
  }
522
- if (!update.encoding)
532
+ if (!update.encoding && !head_override)
523
533
  update.content_type = content_type
524
534
  res.sendUpdate(update)
525
535
  },
@@ -690,7 +700,11 @@ function create_braid_text() {
690
700
 
691
701
  var resource = (typeof key == 'string') ? await get_resource(key) : key
692
702
  var version = resource.version
693
- var merge_type = options.range_unit === 'yjs-text' ? 'yjs'
703
+ // head: true = subscribe to header-only updates (version/parents,
704
+ // no body or patches). Cheap: never materializes history or patch
705
+ // content. Head clients live in the dt client list.
706
+ var merge_type = options.head ? 'dt'
707
+ : options.range_unit === 'yjs-text' ? 'yjs'
694
708
  : (options.merge_type || 'simpleton')
695
709
  var has_parents = Array.isArray(options.parents)
696
710
  var has_version = Array.isArray(options.version)
@@ -829,12 +843,20 @@ function create_braid_text() {
829
843
  var client = {
830
844
  merge_type: 'dt',
831
845
  peer: options.peer,
846
+ head: !!options.head,
832
847
  send_update: one_at_a_time(options.subscribe),
833
848
  accept_encoding_dt: !!options.accept_encoding?.match(/updates\s*\((.*)\)/)?.[1]?.split(',').map(x=>x.trim()).includes('dt'),
834
849
  }
835
850
 
836
851
  // Send initial history
837
- if (client.accept_encoding_dt) {
852
+ if (client.head) {
853
+ // Header-only: announce the current frontier in a single
854
+ // update — or send nothing if the subscriber's parents
855
+ // are already current (getting.history === false)
856
+ if (getting.history)
857
+ client.send_update({ version: resource.version,
858
+ parents: options.parents || [] })
859
+ } else if (client.accept_encoding_dt) {
838
860
  if (!getting.history)
839
861
  client.send_update({ encoding: 'dt', body: new Doc().toBytes() })
840
862
  else {
@@ -993,7 +1015,11 @@ function create_braid_text() {
993
1015
  for (let client of resource.dt.clients) {
994
1016
  if (!peer || client.peer !== peer)
995
1017
  await client.send_update(
996
- client.accept_encoding_dt
1018
+ client.head
1019
+ ? { version: resource.version,
1020
+ parents: version_before_yjs_sync
1021
+ }
1022
+ : client.accept_encoding_dt
997
1023
  ? { version: resource.version,
998
1024
  parents: version_before_yjs_sync,
999
1025
  body: resource.dt.doc.getPatchSince(yjs_v_before),
@@ -1042,6 +1068,7 @@ function create_braid_text() {
1042
1068
 
1043
1069
  if (options.transfer_encoding === 'dt') {
1044
1070
  await ensure_dt_exists(resource)
1071
+ var version_before_dt_put = resource.version
1045
1072
  var start_i = 1 + resource.dt.doc.getLocalVersion().reduce((a, b) => Math.max(a, b), -1)
1046
1073
 
1047
1074
  resource.dt.doc.mergeBytes(body)
@@ -1059,9 +1086,10 @@ function create_braid_text() {
1059
1086
 
1060
1087
  // Notify non-simpleton clients with the dt-encoded update
1061
1088
  var dt_update = { body, encoding: 'dt' }
1089
+ var head_update = { version: resource.version, parents: version_before_dt_put }
1062
1090
  for (let client of resource.dt.clients)
1063
1091
  if (!peer || client.peer !== peer)
1064
- await client.send_update(dt_update)
1092
+ await client.send_update(client.head ? head_update : dt_update)
1065
1093
 
1066
1094
  return { dt: { change_count: end_i - start_i + 1 } }
1067
1095
  }
@@ -1355,9 +1383,10 @@ function create_braid_text() {
1355
1383
  content: p.content
1356
1384
  })),
1357
1385
  }
1386
+ var x_head = { version: x.version, parents: x.parents }
1358
1387
  for (let client of resource.dt.clients) {
1359
1388
  if (!peer || client.peer !== peer)
1360
- post_commit_updates.push([client, x])
1389
+ post_commit_updates.push([client, client.head ? x_head : x])
1361
1390
  }
1362
1391
 
1363
1392
  await resource.dt.log.save(resource.dt.doc.getPatchSince(v_before))