braid-text 0.2.85 → 0.2.87

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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(node test/test.js:*)"
5
+ ],
6
+ "deny": [],
7
+ "ask": []
8
+ }
9
+ }
package/index.js CHANGED
@@ -165,13 +165,18 @@ function create_braid_text() {
165
165
  signal: ac.signal,
166
166
  subscribe: update => {
167
167
  update.signal = ac.signal
168
+ update.dont_retry = true
168
169
  braid_text.put(b, update).then((x) => {
169
- local_first_put()
170
- extend_fork_point(update)
170
+ if (x.ok) {
171
+ local_first_put()
172
+ extend_fork_point(update)
173
+ } else if (x.status === 401 || x.status === 403) {
174
+ options.on_unauthorized?.()
175
+ } else throw new Error('failed to PUT: ' + x.status)
171
176
  }).catch(e => {
172
177
  if (e.name === 'AbortError') {
173
178
  // ignore
174
- } else throw e
179
+ } else handle_error(e)
175
180
  })
176
181
  }
177
182
  }
@@ -180,12 +185,30 @@ function create_braid_text() {
180
185
  braid_text.get(a, a_ops)
181
186
 
182
187
  // remote -> local
188
+ var remote_res_done
189
+ var remote_res_promise = new Promise(done => remote_res_done = done)
190
+ var remote_res = null
191
+
183
192
  var b_ops = {
184
193
  signal: ac.signal,
185
194
  dont_retry: true,
195
+ headers: { 'Merge-Type': 'dt', 'accept-encoding': 'updates(dt)' },
186
196
  subscribe: async update => {
187
- await braid_text.put(a, update)
188
- extend_fork_point(update)
197
+ // Wait for remote_res to be available
198
+ await remote_res_promise
199
+
200
+ // Check if this is a dt-encoded update (initial body without status)
201
+ if (!update.status) {
202
+ var cv = remote_res.headers.get('current-version')
203
+ await braid_text.put(a, {
204
+ body: update.body,
205
+ transfer_encoding: 'dt'
206
+ })
207
+ if (cv) extend_fork_point({ version: JSON.parse(`[${cv}]`), parents: resource.meta.fork_point || [] })
208
+ } else {
209
+ await braid_text.put(a, update)
210
+ if (update.version) extend_fork_point(update)
211
+ }
189
212
  },
190
213
  on_error: e => {
191
214
  options.on_disconnect?.()
@@ -193,14 +216,16 @@ function create_braid_text() {
193
216
  }
194
217
  }
195
218
  // Handle case where remote doesn't exist yet - wait for local to create it
196
- var remote_result = await braid_text.get(b, b_ops)
197
- if (remote_result === null) {
219
+ remote_res = await braid_text.get(b, b_ops)
220
+ remote_res_done()
221
+ if (remote_res === null) {
198
222
  // Remote doesn't exist yet, wait for local to put something
199
223
  await local_first_put_promise
200
224
  disconnect()
201
225
  connect()
226
+ return
202
227
  }
203
- options.on_res?.(remote_result)
228
+ options.on_res?.(remote_res)
204
229
  // on_error will call handle_error when connection drops
205
230
  } catch (e) {
206
231
  handle_error(e)
@@ -542,9 +567,12 @@ function create_braid_text() {
542
567
 
543
568
  if (options.subscribe) {
544
569
  res.subscribe(async update => {
545
- update.body = update.body_text
546
- if (update.patches)
547
- for (var p of update.patches) p.content = p.content_text
570
+ // Don't convert to text for initial dt-encoded body (no status)
571
+ if (update.status) {
572
+ update.body = update.body_text
573
+ if (update.patches)
574
+ for (var p of update.patches) p.content = p.content_text
575
+ }
548
576
  await options.subscribe(update)
549
577
  }, e => options.on_error?.(e))
550
578
 
@@ -705,8 +733,9 @@ function create_braid_text() {
705
733
  var params = {
706
734
  method: 'PUT',
707
735
  signal: options.signal,
708
- retry: () => true,
709
736
  }
737
+ if (!options.dont_retry)
738
+ params.retry = () => true
710
739
  for (var x of ['headers', 'parents', 'version', 'peer', 'body', 'patches'])
711
740
  if (options[x] != null) params[x] = options[x]
712
741
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-text",
3
- "version": "0.2.85",
3
+ "version": "0.2.87",
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/test/test.js CHANGED
@@ -69,6 +69,21 @@ function createTestServer(options = {}) {
69
69
  return res.end('error')
70
70
  }
71
71
 
72
+ if (req.url.startsWith('/unauthorized') && req.method === 'PUT') {
73
+ res.statusCode = 401
74
+ return res.end('Unauthorized')
75
+ }
76
+
77
+ if (req.url.startsWith('/forbidden') && req.method === 'PUT') {
78
+ res.statusCode = 403
79
+ return res.end('Forbidden')
80
+ }
81
+
82
+ if (req.url.startsWith('/server_error') && req.method === 'PUT') {
83
+ res.statusCode = 500
84
+ return res.end('Internal Server Error')
85
+ }
86
+
72
87
  if (req.url.startsWith('/404')) {
73
88
  res.statusCode = 404
74
89
  return res.end('Not Found')
package/test/tests.js CHANGED
@@ -2066,6 +2066,173 @@ runTest(
2066
2066
  'on_res called'
2067
2067
  )
2068
2068
 
2069
+ runTest(
2070
+ "test braid_text.sync uses accept-encoding updates(dt)",
2071
+ async () => {
2072
+ var remote_key = 'test-remote-' + Math.random().toString(36).slice(2)
2073
+ var local_key = 'test-local-' + Math.random().toString(36).slice(2)
2074
+
2075
+ // Create the remote resource with some content
2076
+ var r = await braid_fetch(`/${remote_key}`, {
2077
+ method: 'PUT',
2078
+ body: 'remote content here'
2079
+ })
2080
+ if (!r.ok) return 'put failed: ' + r.status
2081
+
2082
+ // Start sync with URL first (like the passing "url to key" test)
2083
+ var r = await braid_fetch(`/eval`, {
2084
+ method: 'PUT',
2085
+ body: `void (async () => {
2086
+ braid_text.sync(new URL('http://localhost:8889/${remote_key}'), '/${local_key}')
2087
+ res.end('')
2088
+ })()`
2089
+ })
2090
+ if (!r.ok) return 'eval failed: ' + r.status
2091
+
2092
+ // Wait for sync to complete
2093
+ await new Promise(done => setTimeout(done, 100))
2094
+
2095
+ // Check local has remote content
2096
+ var r = await braid_fetch(`/${local_key}`)
2097
+ return await r.text()
2098
+ },
2099
+ 'remote content here'
2100
+ )
2101
+
2102
+ runTest(
2103
+ "test braid_text.sync reconnects when inner put fails with non-200 status",
2104
+ async () => {
2105
+ var key = 'test-' + Math.random().toString(36).slice(2)
2106
+
2107
+ // Create a local resource with content
2108
+ var r = await braid_fetch(`/${key}`, {
2109
+ method: 'PUT',
2110
+ body: 'initial'
2111
+ })
2112
+ if (!r.ok) return 'initial put failed: ' + r.status
2113
+
2114
+ var r = await braid_fetch(`/eval`, {
2115
+ method: 'PUT',
2116
+ body: `void (async () => {
2117
+ var connect_count = 0
2118
+ var ac = new AbortController()
2119
+
2120
+ braid_text.sync('/${key}', new URL('http://localhost:8889/server_error'), {
2121
+ signal: ac.signal,
2122
+ on_pre_connect: () => {
2123
+ connect_count++
2124
+ if (connect_count >= 2) {
2125
+ ac.abort()
2126
+ res.end('reconnected after put failure')
2127
+ }
2128
+ }
2129
+ })
2130
+
2131
+ // Trigger a local put which will fail when synced to the error endpoint
2132
+ await new Promise(done => setTimeout(done, 100))
2133
+ await braid_text.put('/${key}', { body: 'trigger sync' })
2134
+
2135
+ // Wait for reconnect attempt
2136
+ await new Promise(done => setTimeout(done, 2000))
2137
+ ac.abort()
2138
+ res.end('did not reconnect')
2139
+ })()`
2140
+ })
2141
+ if (!r.ok) return 'eval failed: ' + r.status
2142
+
2143
+ return await r.text()
2144
+ },
2145
+ 'reconnected after put failure'
2146
+ )
2147
+
2148
+ runTest(
2149
+ "test braid_text.sync on_unauthorized callback for 401",
2150
+ async () => {
2151
+ var key = 'test-' + Math.random().toString(36).slice(2)
2152
+
2153
+ // Create a local resource with content
2154
+ var r = await braid_fetch(`/${key}`, {
2155
+ method: 'PUT',
2156
+ body: 'initial'
2157
+ })
2158
+ if (!r.ok) return 'initial put failed: ' + r.status
2159
+
2160
+ var r = await braid_fetch(`/eval`, {
2161
+ method: 'PUT',
2162
+ body: `void (async () => {
2163
+ var unauthorized_called = false
2164
+ var ac = new AbortController()
2165
+
2166
+ braid_text.sync('/${key}', new URL('http://localhost:8889/unauthorized'), {
2167
+ signal: ac.signal,
2168
+ on_unauthorized: () => {
2169
+ unauthorized_called = true
2170
+ ac.abort()
2171
+ res.end('on_unauthorized called')
2172
+ }
2173
+ })
2174
+
2175
+ // Trigger a local put which will get 401 when synced
2176
+ await new Promise(done => setTimeout(done, 100))
2177
+ await braid_text.put('/${key}', { body: 'trigger sync' })
2178
+
2179
+ // Wait for callback
2180
+ await new Promise(done => setTimeout(done, 2000))
2181
+ ac.abort()
2182
+ res.end('on_unauthorized not called')
2183
+ })()`
2184
+ })
2185
+ if (!r.ok) return 'eval failed: ' + r.status
2186
+
2187
+ return await r.text()
2188
+ },
2189
+ 'on_unauthorized called'
2190
+ )
2191
+
2192
+ runTest(
2193
+ "test braid_text.sync on_unauthorized callback for 403",
2194
+ async () => {
2195
+ var key = 'test-' + Math.random().toString(36).slice(2)
2196
+
2197
+ // Create a local resource with content
2198
+ var r = await braid_fetch(`/${key}`, {
2199
+ method: 'PUT',
2200
+ body: 'initial'
2201
+ })
2202
+ if (!r.ok) return 'initial put failed: ' + r.status
2203
+
2204
+ var r = await braid_fetch(`/eval`, {
2205
+ method: 'PUT',
2206
+ body: `void (async () => {
2207
+ var unauthorized_called = false
2208
+ var ac = new AbortController()
2209
+
2210
+ braid_text.sync('/${key}', new URL('http://localhost:8889/forbidden'), {
2211
+ signal: ac.signal,
2212
+ on_unauthorized: () => {
2213
+ unauthorized_called = true
2214
+ ac.abort()
2215
+ res.end('on_unauthorized called')
2216
+ }
2217
+ })
2218
+
2219
+ // Trigger a local put which will get 403 when synced
2220
+ await new Promise(done => setTimeout(done, 100))
2221
+ await braid_text.put('/${key}', { body: 'trigger sync' })
2222
+
2223
+ // Wait for callback
2224
+ await new Promise(done => setTimeout(done, 2000))
2225
+ ac.abort()
2226
+ res.end('on_unauthorized not called')
2227
+ })()`
2228
+ })
2229
+ if (!r.ok) return 'eval failed: ' + r.status
2230
+
2231
+ return await r.text()
2232
+ },
2233
+ 'on_unauthorized called'
2234
+ )
2235
+
2069
2236
  runTest(
2070
2237
  "test getting a binary update from a subscription",
2071
2238
  async () => {