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.
- package/.claude/settings.local.json +9 -0
- package/index.js +41 -12
- package/package.json +1 -1
- package/test/test.js +15 -0
- package/test/tests.js +167 -0
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
|
-
|
|
170
|
-
|
|
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
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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?.(
|
|
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
|
-
|
|
546
|
-
if (update.
|
|
547
|
-
|
|
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
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 () => {
|