braid-text 0.5.6 → 0.5.7

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.
@@ -55,6 +55,18 @@
55
55
  //
56
56
  // content_type: used for Accept and Content-Type headers
57
57
  //
58
+ // on_error?: (error) => void
59
+ // called when an error occurs (e.g., network failure, digest mismatch)
60
+ //
61
+ // on_online?: (is_online) => void
62
+ // called when the connection status changes
63
+ //
64
+ // on_ack?: () => void
65
+ // called when all outstanding PUTs have been acknowledged
66
+ //
67
+ // send_digests?: boolean
68
+ // if truthy, includes a Repr-Digest header with each PUT
69
+ //
58
70
  // returns { changed, abort }
59
71
  // call changed() whenever there is a local change,
60
72
  // and the system will call a combination of get_state and
@@ -106,10 +118,9 @@ function simpleton_client(url, {
106
118
  get_patches,
107
119
  get_state,
108
120
  content_type,
109
- headers: custom_headers, // The user can pass in custom headers
110
- // that are forwared into the fetch
121
+ headers, // The user can pass in custom headers
122
+ // that are forwarded into fetches
111
123
  on_error,
112
- on_res,
113
124
  on_online,
114
125
  on_ack,
115
126
  send_digests
@@ -122,38 +133,34 @@ function simpleton_client(url, {
122
133
  var max_outstanding_changes = 10 // throttle limit
123
134
  var throttled = false
124
135
  var throttled_updates = []
125
- var ac = new AbortController()
126
136
 
127
- // ── Subscription (GET) ──────────────────────────────────────────────
128
- //
129
- // Opens a long-lived GET subscription with retry: () => true, meaning
130
- // any disconnection (network error, HTTP error) triggers automatic
131
- // reconnection with backoff.
132
- //
133
- // The parents callback sends client_version on reconnect, so the
134
- // server knows where we left off and can send patches from there.
135
- //
136
- // IMPORTANT: No changed() / flush is called on reconnect. The
137
- // subscription simply resumes. Any queued PUTs are retried by
138
- // braid_fetch independently.
139
- braid_fetch(url, {
140
- peer,
141
- subscribe: true,
142
- heartbeats: 20,
143
- signal: ac.signal,
144
- retry: () => true,
145
- parents: () => client_version.length ? client_version : null,
146
- onSubscriptionStatus: status => on_online && on_online(status.online),
147
- headers: { ...custom_headers,
148
- "Merge-Type": "simpleton",
149
- ...content_type && {Accept: content_type} },
150
- }).then(res => {
151
- if (on_res) on_res(res)
152
- res.subscribe(async update => {
137
+ // extend the headers with merge-type and peer
138
+ headers = {
139
+ ...headers,
140
+ "Merge-Type": "simpleton",
141
+ Peer: peer,
142
+ }
143
+
144
+ // Manages both the GET subscription and PUT requests through a single
145
+ // channel with automatic reconnection and PUT queuing.
146
+ var channel = reliable_update_channel(url, {
147
+ reconnect_from_parents: () => client_version.length ? client_version : null,
148
+ get_headers: {
149
+ ...headers,
150
+ ...content_type && {Accept: content_type}
151
+ },
152
+ put_headers: {
153
+ ...headers,
154
+ ...content_type && {"Content-Type": content_type}
155
+ },
156
+ on_update: async update => {
153
157
  // ── Parent check ────────────────────────────────────────
154
158
  // Core simpleton invariant: only accept updates whose
155
- // parents match our client_version exactly. This ensures
156
- // we stay on a single line of time.
159
+ // parents form a continuous chain. We compare against the
160
+ // last queued update's version (if throttled) or
161
+ // client_version. When throttled, matching updates are
162
+ // queued but not applied — they'll be applied later when
163
+ // the throttle clears (see changed()).
157
164
  update.parents.sort()
158
165
  var last_queued = throttled_updates.length
159
166
  ? throttled_updates[throttled_updates.length - 1].version
@@ -161,8 +168,16 @@ function simpleton_client(url, {
161
168
  if (versions_eq(last_queued, update.parents))
162
169
  if (throttled) throttled_updates.push(update)
163
170
  else await apply_update(update)
164
- }, on_error)
165
- }).catch(on_error)
171
+ },
172
+ on_status: status => on_online && on_online(status.online),
173
+ on_error: err => on_error && on_error(err),
174
+
175
+ // this api is preliminary and undocumented;
176
+ // we use it to tell the reliable_update_channel to die,
177
+ // if there is a digest mismatch on the server,
178
+ // which will result in a 550 status code
179
+ no_retry_status_codes: [550]
180
+ })
166
181
 
167
182
  async function apply_update(update) {
168
183
  // ── Parse and convert patches ───────────────────────────────
@@ -246,7 +261,7 @@ function simpleton_client(url, {
246
261
  // ── Public interface ────────────────────────────────────────────────
247
262
  return {
248
263
  // ── abort() — cancel the subscription ─────────────────────────
249
- abort: () => ac.abort(),
264
+ abort: () => channel.close(),
250
265
 
251
266
  // ── changed() — call when local edits occur ───────────────────
252
267
  // This is the entry point for sending local edits. It:
@@ -326,28 +341,15 @@ function simpleton_client(url, {
326
341
  // - HTTP 550 (Repr-Digest mismatch / out of sync):
327
342
  // give up, throw — client must be re-created
328
343
  outstanding_changes++
329
- try {
330
- var r = await braid_fetch(url, {
331
- method: "PUT",
332
- peer, version, parents, patches,
333
- retry: (res) => res.status !== 550,
334
- headers: {
335
- ...custom_headers,
336
- "Merge-Type": "simpleton",
337
- ...send_digests && {
338
- "Repr-Digest": await get_digest(client_state) },
339
- ...content_type && {
340
- "Content-Type": content_type }
341
- }
342
- })
343
- if (!r.ok) throw new Error(`bad http status: ${r.status}`)
344
- } catch (e) {
345
- // A 550 means Repr-Digest check failed — we're out
346
- // of sync. The client must be torn down and
347
- // re-created from scratch.
348
- on_error(e)
349
- throw e
350
- }
344
+
345
+ await channel.put({
346
+ version, parents, patches,
347
+ headers: {
348
+ ...send_digests && {
349
+ "Repr-Digest": await get_digest(client_state) }
350
+ }
351
+ })
352
+
351
353
  throttled = false
352
354
  outstanding_changes--
353
355
  if (on_ack && !outstanding_changes) on_ack()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-text",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "description": "Library for collaborative text over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braid-text",
@@ -25,7 +25,7 @@
25
25
  ],
26
26
  "dependencies": {
27
27
  "@braid.org/diamond-types-node": "^2.0.1",
28
- "braid-http": "^1.3.106"
28
+ "braid-http": "~1.3.123"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "yjs": "^13.6.0"
package/server.js CHANGED
@@ -959,7 +959,8 @@ function create_braid_text() {
959
959
  }
960
960
  for (var b of dt_bytes) resource.dt.doc.mergeBytes(b)
961
961
  resource.version = resource.dt.doc.getRemoteVersion().map(x => x.join("-")).sort()
962
- if (!resource.dt.known_versions[syn_actor]) resource.dt.known_versions[syn_actor] = new RangeSet()
962
+ if (!resource.dt.known_versions[syn_actor])
963
+ resource.dt.known_versions[syn_actor] = new RangeSet()
963
964
  resource.dt.known_versions[syn_actor].add_range(0, syn_seq - 1)
964
965
  await resource.dt.log.save(resource.dt.doc.getPatchSince(yjs_v_before))
965
966
 
@@ -977,8 +978,15 @@ function create_braid_text() {
977
978
  if (!peer || client.peer !== peer)
978
979
  await client.send_update(
979
980
  client.accept_encoding_dt
980
- ? { version: resource.version, parents: version_before_yjs_sync, body: resource.dt.doc.getPatchSince(yjs_v_before), encoding: 'dt' }
981
- : { version: resource.version, parents: version_before_yjs_sync, patches: xf }
981
+ ? { version: resource.version,
982
+ parents: version_before_yjs_sync,
983
+ body: resource.dt.doc.getPatchSince(yjs_v_before),
984
+ encoding: 'dt'
985
+ }
986
+ : { version: resource.version,
987
+ parents: version_before_yjs_sync,
988
+ patches: xf
989
+ }
982
990
  )
983
991
  }
984
992
  }
@@ -1349,7 +1357,9 @@ function create_braid_text() {
1349
1357
  if (braid_text.db_folder) {
1350
1358
  await db_folder_init()
1351
1359
  var pages = new Set()
1352
- for (let x of await require('fs').promises.readdir(braid_text.db_folder)) if (/\.(dt|yjs)\.\d+$/.test(x)) pages.add(decode_filename(x.replace(/\.(dt|yjs)\.\d+$/, '')))
1360
+ for (let x of await require('fs').promises.readdir(braid_text.db_folder))
1361
+ if (/\.(dt|yjs)\.\d+$/.test(x))
1362
+ pages.add(decode_filename(x.replace(/\.(dt|yjs)\.\d+$/, '')))
1353
1363
  return [...pages.keys()]
1354
1364
  } else return Object.keys(braid_text.cache)
1355
1365
  } catch (e) { return [] }