braid-blob 0.0.38 → 0.0.40
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/index.js +325 -322
- package/package.json +1 -1
- package/test/tests.js +35 -78
package/index.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
var {http_server: braidify, fetch: braid_fetch} = require('braid-http')
|
|
2
|
-
fs = require('fs'),
|
|
3
|
-
path = require('path')
|
|
1
|
+
var {http_server: braidify, fetch: braid_fetch} = require('braid-http')
|
|
4
2
|
|
|
5
3
|
function create_braid_blob() {
|
|
6
4
|
var braid_blob = {
|
|
@@ -21,29 +19,29 @@ function create_braid_blob() {
|
|
|
21
19
|
|
|
22
20
|
async function real_init() {
|
|
23
21
|
// Ensure our meta folder exists
|
|
24
|
-
await fs.promises.mkdir(braid_blob.meta_folder, { recursive: true })
|
|
22
|
+
await require('fs').promises.mkdir(braid_blob.meta_folder, { recursive: true })
|
|
25
23
|
|
|
26
24
|
// Set up db - either use provided object or create file-based storage
|
|
27
25
|
if (typeof braid_blob.db_folder === 'string') {
|
|
28
|
-
await fs.promises.mkdir(braid_blob.db_folder, { recursive: true })
|
|
26
|
+
await require('fs').promises.mkdir(braid_blob.db_folder, { recursive: true })
|
|
29
27
|
braid_blob.db = {
|
|
30
28
|
read: async (key) => {
|
|
31
|
-
var file_path =
|
|
29
|
+
var file_path = `${braid_blob.db_folder}/${encode_filename(key)}`
|
|
32
30
|
try {
|
|
33
|
-
return await fs.promises.readFile(file_path)
|
|
31
|
+
return await require('fs').promises.readFile(file_path)
|
|
34
32
|
} catch (e) {
|
|
35
33
|
if (e.code === 'ENOENT') return null
|
|
36
34
|
throw e
|
|
37
35
|
}
|
|
38
36
|
},
|
|
39
37
|
write: async (key, data) => {
|
|
40
|
-
var file_path =
|
|
41
|
-
await fs.promises.writeFile(file_path, data)
|
|
38
|
+
var file_path = `${braid_blob.db_folder}/${encode_filename(key)}`
|
|
39
|
+
await require('fs').promises.writeFile(file_path, data)
|
|
42
40
|
},
|
|
43
41
|
delete: async (key) => {
|
|
44
|
-
var file_path =
|
|
42
|
+
var file_path = `${braid_blob.db_folder}/${encode_filename(key)}`
|
|
45
43
|
try {
|
|
46
|
-
await fs.promises.unlink(file_path)
|
|
44
|
+
await require('fs').promises.unlink(file_path)
|
|
47
45
|
} catch (e) {
|
|
48
46
|
if (e.code !== 'ENOENT') throw e
|
|
49
47
|
}
|
|
@@ -60,32 +58,32 @@ function create_braid_blob() {
|
|
|
60
58
|
}
|
|
61
59
|
}
|
|
62
60
|
|
|
63
|
-
function get_meta(key) {
|
|
64
|
-
if (braid_blob.meta_cache[key])
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
61
|
+
async function get_meta(key) {
|
|
62
|
+
if (!braid_blob.meta_cache[key]) {
|
|
63
|
+
try {
|
|
64
|
+
braid_blob.meta_cache[key] = JSON.parse(
|
|
65
|
+
await require('fs').promises.readFile(
|
|
66
|
+
`${braid_blob.meta_folder}/${encode_filename(key)}`, 'utf8'))
|
|
67
|
+
} catch (e) {
|
|
68
|
+
if (e.code === 'ENOENT')
|
|
69
|
+
braid_blob.meta_cache[key] = {}
|
|
70
|
+
else throw e
|
|
71
|
+
}
|
|
73
72
|
}
|
|
73
|
+
return braid_blob.meta_cache[key]
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
async function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
var meta_path = path.join(braid_blob.meta_folder, encode_filename(key))
|
|
81
|
-
await fs.promises.writeFile(meta_path, JSON.stringify(meta))
|
|
76
|
+
async function save_meta(key) {
|
|
77
|
+
await require('fs').promises.writeFile(
|
|
78
|
+
`${braid_blob.meta_folder}/${encode_filename(key)}`,
|
|
79
|
+
JSON.stringify(braid_blob.meta_cache[key]))
|
|
82
80
|
}
|
|
83
81
|
|
|
84
82
|
async function delete_meta(key) {
|
|
85
83
|
delete braid_blob.meta_cache[key]
|
|
86
|
-
var meta_path = path.join(braid_blob.meta_folder, encode_filename(key))
|
|
87
84
|
try {
|
|
88
|
-
await fs.promises.unlink(
|
|
85
|
+
await require('fs').promises.unlink(
|
|
86
|
+
`${braid_blob.meta_folder}/${encode_filename(key)}`)
|
|
89
87
|
} catch (e) {
|
|
90
88
|
if (e.code !== 'ENOENT') throw e
|
|
91
89
|
}
|
|
@@ -96,11 +94,10 @@ function create_braid_blob() {
|
|
|
96
94
|
|
|
97
95
|
// Handle URL case - make a remote PUT request
|
|
98
96
|
if (key instanceof URL) {
|
|
99
|
-
|
|
100
97
|
var params = {
|
|
101
98
|
method: 'PUT',
|
|
102
99
|
signal: options.signal,
|
|
103
|
-
body
|
|
100
|
+
body
|
|
104
101
|
}
|
|
105
102
|
if (!options.dont_retry)
|
|
106
103
|
params.retry = () => true
|
|
@@ -116,48 +113,43 @@ function create_braid_blob() {
|
|
|
116
113
|
await braid_blob.init()
|
|
117
114
|
if (options.signal?.aborted) return
|
|
118
115
|
|
|
119
|
-
|
|
116
|
+
return await within_fiber(key, async () => {
|
|
117
|
+
var meta = await get_meta(key)
|
|
118
|
+
if (options.signal?.aborted) return
|
|
120
119
|
|
|
121
|
-
|
|
122
|
-
!options.version ?
|
|
120
|
+
var their_e = options.version ? options.version[0] :
|
|
123
121
|
// we'll give them a event id in this case
|
|
124
|
-
`${braid_blob.peer}-${
|
|
125
|
-
meta.event ?
|
|
126
|
-
!options.version.length ?
|
|
127
|
-
null :
|
|
128
|
-
options.version[0]
|
|
129
|
-
|
|
130
|
-
if (their_e != null &&
|
|
131
|
-
(meta.event == null ||
|
|
132
|
-
compare_events(their_e, meta.event) > 0)) {
|
|
133
|
-
meta.event = their_e
|
|
134
|
-
|
|
135
|
-
// Write the file using url-file-db (unless skip_write is set)
|
|
136
|
-
if (!options.skip_write)
|
|
137
|
-
await braid_blob.db.write(key, body)
|
|
138
|
-
if (options.signal?.aborted) return
|
|
122
|
+
`${braid_blob.peer}-${max_seq('' + Date.now(),
|
|
123
|
+
meta.event ? increment_seq(get_event_seq(meta.event)) : '')}`
|
|
139
124
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (options.content_type)
|
|
143
|
-
meta_updates.content_type = options.content_type
|
|
125
|
+
if (compare_events(their_e, meta.event) > 0) {
|
|
126
|
+
meta.event = their_e
|
|
144
127
|
|
|
145
|
-
|
|
146
|
-
|
|
128
|
+
if (!options.skip_write)
|
|
129
|
+
await braid_blob.db.write(key, body)
|
|
130
|
+
if (options.signal?.aborted) return
|
|
147
131
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
sub.sendUpdate({
|
|
154
|
-
version: [meta.event],
|
|
155
|
-
'Merge-Type': 'aww',
|
|
156
|
-
body
|
|
157
|
-
})
|
|
158
|
-
}
|
|
132
|
+
if (options.content_type)
|
|
133
|
+
meta.content_type = options.content_type
|
|
134
|
+
|
|
135
|
+
await save_meta(key)
|
|
136
|
+
if (options.signal?.aborted) return
|
|
159
137
|
|
|
160
|
-
|
|
138
|
+
// Notify all subscriptions of the update
|
|
139
|
+
// (except the peer which made the PUT request itself)
|
|
140
|
+
var update = {
|
|
141
|
+
version: [meta.event],
|
|
142
|
+
content_type: meta.content_type,
|
|
143
|
+
body
|
|
144
|
+
}
|
|
145
|
+
if (braid_blob.key_to_subs[key])
|
|
146
|
+
for (var [peer, sub] of braid_blob.key_to_subs[key].entries())
|
|
147
|
+
if (!options.peer || options.peer !== peer)
|
|
148
|
+
await sub.sendUpdate(update)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return meta.event
|
|
152
|
+
})
|
|
161
153
|
}
|
|
162
154
|
|
|
163
155
|
braid_blob.get = async (key, options = {}) => {
|
|
@@ -167,7 +159,7 @@ function create_braid_blob() {
|
|
|
167
159
|
if (key instanceof URL) {
|
|
168
160
|
var params = {
|
|
169
161
|
signal: options.signal,
|
|
170
|
-
subscribe:
|
|
162
|
+
subscribe: options.subscribe,
|
|
171
163
|
heartbeats: 120,
|
|
172
164
|
}
|
|
173
165
|
if (!options.dont_retry) {
|
|
@@ -183,7 +175,9 @@ function create_braid_blob() {
|
|
|
183
175
|
|
|
184
176
|
var res = await braid_fetch(key.href, params)
|
|
185
177
|
|
|
186
|
-
if (!res.ok)
|
|
178
|
+
if (!res.ok)
|
|
179
|
+
if (options.subscribe) throw new Error('failed to subscribe')
|
|
180
|
+
else return null
|
|
187
181
|
|
|
188
182
|
var result = {}
|
|
189
183
|
if (res.version) result.version = res.version
|
|
@@ -192,6 +186,8 @@ function create_braid_blob() {
|
|
|
192
186
|
|
|
193
187
|
if (options.subscribe) {
|
|
194
188
|
res.subscribe(async update => {
|
|
189
|
+
if (update.status === 404) update.delete = true
|
|
190
|
+
update.content_type = update.extra_headers['content-type']
|
|
195
191
|
await options.subscribe(update)
|
|
196
192
|
}, e => options.on_error?.(e))
|
|
197
193
|
return res
|
|
@@ -202,70 +198,72 @@ function create_braid_blob() {
|
|
|
202
198
|
}
|
|
203
199
|
|
|
204
200
|
await braid_blob.init()
|
|
201
|
+
if (options.signal?.aborted) return
|
|
205
202
|
|
|
206
|
-
|
|
207
|
-
|
|
203
|
+
return await within_fiber(key, async () => {
|
|
204
|
+
var meta = await get_meta(key)
|
|
205
|
+
if (options.signal?.aborted) return
|
|
208
206
|
|
|
209
|
-
|
|
210
|
-
version: [meta.event],
|
|
211
|
-
content_type: meta.content_type || options.content_type
|
|
212
|
-
}
|
|
213
|
-
if (options.header_cb) await options.header_cb(result)
|
|
214
|
-
if (options.signal?.aborted) return
|
|
215
|
-
// Check if requested version/parents is newer than what we have - if so, we don't have it
|
|
216
|
-
if (options.version && options.version.length && compare_events(options.version[0], meta.event) > 0)
|
|
217
|
-
throw new Error('unknown version: ' + options.version)
|
|
218
|
-
if (options.parents && options.parents.length && compare_events(options.parents[0], meta.event) > 0)
|
|
219
|
-
throw new Error('unknown version: ' + options.parents)
|
|
220
|
-
if (options.head) return result
|
|
221
|
-
|
|
222
|
-
if (options.subscribe) {
|
|
223
|
-
var subscribe_chain = Promise.resolve()
|
|
224
|
-
options.my_subscribe = (x) => subscribe_chain =
|
|
225
|
-
subscribe_chain.then(() =>
|
|
226
|
-
!options.signal?.aborted && options.subscribe(x))
|
|
227
|
-
|
|
228
|
-
// Start a subscription for future updates
|
|
229
|
-
if (!braid_blob.key_to_subs[key])
|
|
230
|
-
braid_blob.key_to_subs[key] = new Map()
|
|
231
|
-
|
|
232
|
-
var peer = options.peer || Math.random().toString(36).slice(2)
|
|
233
|
-
braid_blob.key_to_subs[key].set(peer, {
|
|
234
|
-
sendUpdate: (update) => {
|
|
235
|
-
options.my_subscribe({
|
|
236
|
-
body: update.body,
|
|
237
|
-
version: update.version,
|
|
238
|
-
content_type: meta.content_type || options.content_type
|
|
239
|
-
})
|
|
240
|
-
}
|
|
241
|
-
})
|
|
207
|
+
if (!meta.event && !options.subscribe) return null
|
|
242
208
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
})
|
|
209
|
+
var result = {
|
|
210
|
+
version: meta.event ? [meta.event] : [],
|
|
211
|
+
content_type: meta.content_type
|
|
212
|
+
}
|
|
248
213
|
|
|
249
|
-
if (options.
|
|
214
|
+
if (options.header_cb) await options.header_cb(result)
|
|
250
215
|
if (options.signal?.aborted) return
|
|
251
216
|
|
|
252
|
-
//
|
|
253
|
-
if (!options.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
217
|
+
// Check if requested version/parents is newer than what we have - if so, we don't have it
|
|
218
|
+
if (!options.subscribe) {
|
|
219
|
+
if (compare_events(options.version?.[0], meta.event) > 0)
|
|
220
|
+
throw new Error('unknown version: ' + options.version)
|
|
221
|
+
if (compare_events(options.parents?.[0], meta.event) > 0)
|
|
222
|
+
throw new Error('unknown version: ' + options.parents)
|
|
223
|
+
}
|
|
224
|
+
if (options.head) return result
|
|
225
|
+
|
|
226
|
+
if (options.subscribe) {
|
|
227
|
+
var subscribe_chain = Promise.resolve()
|
|
228
|
+
options.my_subscribe = (x) => subscribe_chain =
|
|
229
|
+
subscribe_chain.then(() =>
|
|
230
|
+
!options.signal?.aborted && options.subscribe(x))
|
|
231
|
+
|
|
232
|
+
// Start a subscription for future updates
|
|
233
|
+
if (!braid_blob.key_to_subs[key])
|
|
234
|
+
braid_blob.key_to_subs[key] = new Map()
|
|
235
|
+
|
|
236
|
+
var peer = options.peer || Math.random().toString(36).slice(2)
|
|
237
|
+
braid_blob.key_to_subs[key].set(peer, {
|
|
238
|
+
sendUpdate: (update) => {
|
|
239
|
+
if (update.delete) options.my_subscribe(update)
|
|
240
|
+
else if (compare_events(update.version[0], options.parents?.[0]) > 0)
|
|
241
|
+
options.my_subscribe(update)
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
options.signal?.addEventListener('abort', () => {
|
|
246
|
+
braid_blob.key_to_subs[key].delete(peer)
|
|
247
|
+
if (!braid_blob.key_to_subs[key].size)
|
|
248
|
+
delete braid_blob.key_to_subs[key]
|
|
261
249
|
})
|
|
250
|
+
|
|
251
|
+
if (options.before_send_cb) await options.before_send_cb()
|
|
252
|
+
if (options.signal?.aborted) return
|
|
253
|
+
|
|
254
|
+
// Send an immediate update if needed
|
|
255
|
+
if (compare_events(result.version?.[0], options.parents?.[0]) > 0) {
|
|
256
|
+
result.sent = true
|
|
257
|
+
result.body = await braid_blob.db.read(key)
|
|
258
|
+
options.my_subscribe(result)
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
// If not subscribe, send the body now
|
|
262
|
+
result.body = await braid_blob.db.read(key)
|
|
262
263
|
}
|
|
263
|
-
} else {
|
|
264
|
-
// If not subscribe, send the body now
|
|
265
|
-
result.body = await braid_blob.db.read(key)
|
|
266
|
-
}
|
|
267
264
|
|
|
268
|
-
|
|
265
|
+
return result
|
|
266
|
+
})
|
|
269
267
|
}
|
|
270
268
|
|
|
271
269
|
braid_blob.delete = async (key, options = {}) => {
|
|
@@ -273,13 +271,18 @@ function create_braid_blob() {
|
|
|
273
271
|
|
|
274
272
|
// Handle URL case - make a remote DELETE request
|
|
275
273
|
if (key instanceof URL) {
|
|
276
|
-
|
|
277
274
|
var params = {
|
|
278
275
|
method: 'DELETE',
|
|
279
276
|
signal: options.signal
|
|
280
277
|
}
|
|
278
|
+
if (!options.dont_retry)
|
|
279
|
+
params.retry = (res) => res.status !== 309 &&
|
|
280
|
+
res.status !== 404 && res.status !== 406
|
|
281
281
|
for (var x of ['headers', 'peer'])
|
|
282
282
|
if (options[x] != null) params[x] = options[x]
|
|
283
|
+
if (options.content_type)
|
|
284
|
+
params.headers = { ...params.headers,
|
|
285
|
+
'Accept': options.content_type }
|
|
283
286
|
|
|
284
287
|
return await braid_fetch(key.href, params)
|
|
285
288
|
}
|
|
@@ -287,14 +290,24 @@ function create_braid_blob() {
|
|
|
287
290
|
await braid_blob.init()
|
|
288
291
|
if (options.signal?.aborted) return
|
|
289
292
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
+
return await within_fiber(key, async () => {
|
|
294
|
+
var meta = await get_meta(key)
|
|
295
|
+
if (options.signal?.aborted) return
|
|
296
|
+
|
|
297
|
+
await braid_blob.db.delete(key)
|
|
298
|
+
await delete_meta(key)
|
|
293
299
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
300
|
+
// Notify all subscriptions of the delete
|
|
301
|
+
// (except the peer which made the DELETE request itself)
|
|
302
|
+
var update = {
|
|
303
|
+
delete: true,
|
|
304
|
+
content_type: meta.content_type
|
|
305
|
+
}
|
|
306
|
+
if (braid_blob.key_to_subs[key])
|
|
307
|
+
for (var [peer, sub] of braid_blob.key_to_subs[key].entries())
|
|
308
|
+
if (!options.peer || options.peer !== peer)
|
|
309
|
+
sub.sendUpdate(update)
|
|
310
|
+
})
|
|
298
311
|
}
|
|
299
312
|
|
|
300
313
|
braid_blob.serve = async (req, res, options = {}) => {
|
|
@@ -309,82 +322,85 @@ function create_braid_blob() {
|
|
|
309
322
|
if (res.is_multiplexer) return
|
|
310
323
|
|
|
311
324
|
// Handle OPTIONS request
|
|
312
|
-
if (req.method === 'OPTIONS') return res.end()
|
|
325
|
+
if (req.method === 'OPTIONS') return res.end()
|
|
313
326
|
|
|
314
327
|
// consume PUT body
|
|
315
328
|
var body = req.method === 'PUT' && await slurp(req)
|
|
316
329
|
|
|
317
|
-
|
|
318
|
-
if (
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
res.setHeader("Merge-Type", "aww")
|
|
330
|
+
if (req.method === 'GET' || req.method === 'HEAD') {
|
|
331
|
+
if (!res.hasHeader("editable")) res.setHeader("Editable", "true")
|
|
332
|
+
if (!req.subscribe) res.setHeader("Accept-Subscribe", "true")
|
|
333
|
+
res.setHeader("Merge-Type", "aww")
|
|
322
334
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
335
|
+
try {
|
|
336
|
+
var result = await braid_blob.get(options.key, {
|
|
337
|
+
peer: req.peer,
|
|
338
|
+
head: req.method === "HEAD",
|
|
339
|
+
version: req.version,
|
|
340
|
+
parents: req.parents,
|
|
341
|
+
header_cb: (result) => {
|
|
342
|
+
res.setHeader((req.subscribe ? "Current-" : "") +
|
|
343
|
+
"Version", version_to_header(result.version))
|
|
344
|
+
if (result.content_type)
|
|
345
|
+
res.setHeader('Content-Type', result.content_type)
|
|
346
|
+
},
|
|
347
|
+
before_send_cb: () => res.startSubscription(),
|
|
348
|
+
subscribe: req.subscribe ? (update) => {
|
|
349
|
+
if (update.delete) {
|
|
350
|
+
update.status = 404
|
|
351
|
+
delete update.delete
|
|
352
|
+
}
|
|
353
|
+
if (update.content_type) {
|
|
354
|
+
update['Content-Type'] = update.content_type
|
|
355
|
+
delete update.content_type
|
|
356
|
+
}
|
|
357
|
+
update['Merge-Type'] = 'aww'
|
|
358
|
+
res.sendUpdate(update)
|
|
359
|
+
} : null
|
|
360
|
+
})
|
|
361
|
+
} catch (e) {
|
|
362
|
+
if (e.message && e.message.startsWith('unknown version')) {
|
|
363
|
+
// Server doesn't have this version
|
|
364
|
+
res.statusCode = 309
|
|
365
|
+
res.statusMessage = 'Version Unknown Here'
|
|
366
|
+
return res.end('')
|
|
367
|
+
} else throw e
|
|
368
|
+
}
|
|
354
369
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
370
|
+
if (!result) {
|
|
371
|
+
res.statusCode = 404
|
|
372
|
+
return res.end('File Not Found')
|
|
373
|
+
}
|
|
359
374
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
375
|
+
if (result.content_type && req.headers.accept &&
|
|
376
|
+
!isAcceptable(result.content_type, req.headers.accept)) {
|
|
377
|
+
res.statusCode = 406
|
|
378
|
+
return res.end(`Content-Type of ${result.content_type} not in Accept: ${req.headers.accept}`)
|
|
379
|
+
}
|
|
365
380
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
} else if (req.method === 'PUT') {
|
|
374
|
-
// Handle PUT request to update binary files
|
|
375
|
-
var event = await braid_blob.put(options.key, body, {
|
|
376
|
-
version: req.version,
|
|
377
|
-
content_type: req.headers['content-type'],
|
|
378
|
-
peer: req.peer
|
|
379
|
-
})
|
|
380
|
-
res.setHeader("Version", version_to_header(event != null ? [event] : []))
|
|
381
|
-
res.end('')
|
|
382
|
-
} else if (req.method === 'DELETE') {
|
|
383
|
-
await braid_blob.delete(options.key)
|
|
384
|
-
res.statusCode = 204 // No Content
|
|
385
|
-
res.end('')
|
|
381
|
+
if (req.method == "HEAD") return res.end('')
|
|
382
|
+
else if (!req.subscribe) return res.end(result.body)
|
|
383
|
+
else {
|
|
384
|
+
// If no immediate update was sent,
|
|
385
|
+
// get the node http code to send headers
|
|
386
|
+
if (!result.sent) res.write('\n\n')
|
|
386
387
|
}
|
|
387
|
-
})
|
|
388
|
+
} else if (req.method === 'PUT') {
|
|
389
|
+
// Handle PUT request to update binary files
|
|
390
|
+
var event = await braid_blob.put(options.key, body, {
|
|
391
|
+
version: req.version,
|
|
392
|
+
content_type: req.headers['content-type'],
|
|
393
|
+
peer: req.peer
|
|
394
|
+
})
|
|
395
|
+
res.setHeader("Version", version_to_header(event != null ? [event] : []))
|
|
396
|
+
res.end('')
|
|
397
|
+
} else if (req.method === 'DELETE') {
|
|
398
|
+
await braid_blob.delete(options.key, {
|
|
399
|
+
content_type: req.headers['content-type'],
|
|
400
|
+
peer: req.peer
|
|
401
|
+
})
|
|
402
|
+
res.end('')
|
|
403
|
+
}
|
|
388
404
|
}
|
|
389
405
|
|
|
390
406
|
braid_blob.sync = (a, b, options = {}) => {
|
|
@@ -392,175 +408,131 @@ function create_braid_blob() {
|
|
|
392
408
|
if (!options.peer) options.peer = Math.random().toString(36).slice(2)
|
|
393
409
|
|
|
394
410
|
if ((a instanceof URL) === (b instanceof URL)) {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
peer: options.peer,
|
|
405
|
-
subscribe: update => {
|
|
406
|
-
braid_blob.put(b, update.body, {
|
|
407
|
-
signal: options.signal,
|
|
411
|
+
braid_blob.get(a, {
|
|
412
|
+
...options,
|
|
413
|
+
subscribe: async update => {
|
|
414
|
+
if (update.delete) await braid_blob.delete(b, {
|
|
415
|
+
...options,
|
|
416
|
+
content_type: update.content_type,
|
|
417
|
+
})
|
|
418
|
+
else await braid_blob.put(b, update.body, {
|
|
419
|
+
...options,
|
|
408
420
|
version: update.version,
|
|
409
|
-
headers: options.headers,
|
|
410
421
|
content_type: update.content_type,
|
|
411
|
-
|
|
412
|
-
}).then(a_first_put)
|
|
422
|
+
})
|
|
413
423
|
}
|
|
414
|
-
}
|
|
415
|
-
braid_blob.get(
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
subscribe: update => {
|
|
425
|
-
braid_blob.put(a, update.body, {
|
|
426
|
-
signal: options.signal,
|
|
424
|
+
})
|
|
425
|
+
braid_blob.get(b, {
|
|
426
|
+
...options,
|
|
427
|
+
subscribe: async update => {
|
|
428
|
+
if (update.delete) await braid_blob.delete(a, {
|
|
429
|
+
...options,
|
|
430
|
+
content_type: update.content_type,
|
|
431
|
+
})
|
|
432
|
+
else await braid_blob.put(a, update.body, {
|
|
433
|
+
...options,
|
|
427
434
|
version: update.version,
|
|
428
|
-
headers: options.headers,
|
|
429
435
|
content_type: update.content_type,
|
|
430
|
-
|
|
431
|
-
}).then(b_first_put)
|
|
436
|
+
})
|
|
432
437
|
}
|
|
433
|
-
}
|
|
434
|
-
braid_blob.get(b, b_ops).then(x =>
|
|
435
|
-
x || a_first_put_promise.then(() =>
|
|
436
|
-
braid_blob.get(b, b_ops)))
|
|
438
|
+
})
|
|
437
439
|
} else {
|
|
438
440
|
// One is local, one is remote - make a=local and b=remote (swap if not)
|
|
439
441
|
if (a instanceof URL) {
|
|
440
442
|
let swap = a; a = b; b = swap
|
|
441
443
|
}
|
|
442
444
|
|
|
443
|
-
var
|
|
444
|
-
|
|
445
|
-
options.signal?.addEventListener('abort', () =>
|
|
446
|
-
{ closed = true; disconnect() })
|
|
447
|
-
|
|
448
|
-
var local_first_put, remote_first_put
|
|
449
|
-
var local_first_put_promise = new Promise(done => local_first_put = done)
|
|
450
|
-
var remote_first_put_promise = new Promise(done => remote_first_put = done)
|
|
445
|
+
var ac = new AbortController()
|
|
446
|
+
options.signal?.addEventListener('abort', () => ac.abort())
|
|
451
447
|
|
|
452
448
|
function handle_error(e) {
|
|
453
|
-
if (
|
|
454
|
-
disconnect()
|
|
449
|
+
if (ac.signal.aborted) return
|
|
455
450
|
console.log(`disconnected, retrying in 1 second`)
|
|
456
451
|
setTimeout(connect, 1000)
|
|
457
452
|
}
|
|
458
453
|
|
|
459
454
|
async function connect() {
|
|
455
|
+
if (ac.signal.aborted) return
|
|
460
456
|
if (options.on_pre_connect) await options.on_pre_connect()
|
|
461
457
|
|
|
462
|
-
var ac = new AbortController()
|
|
463
|
-
disconnect = () => ac.abort()
|
|
464
|
-
|
|
465
458
|
try {
|
|
466
459
|
// Check if remote has our current version (simple fork-point check)
|
|
467
|
-
var local_result = await braid_blob.get(a, {
|
|
468
|
-
signal: ac.signal,
|
|
469
|
-
head: true,
|
|
470
|
-
headers: options.headers,
|
|
471
|
-
content_type: options.content_type,
|
|
472
|
-
peer: options.peer,
|
|
473
|
-
})
|
|
474
|
-
var local_version = local_result ? local_result.version : null
|
|
475
460
|
var server_has_our_version = false
|
|
476
|
-
|
|
461
|
+
var local_version = (await braid_blob.get(a, {
|
|
462
|
+
...options,
|
|
463
|
+
signal: ac.signal,
|
|
464
|
+
head: true
|
|
465
|
+
}))?.version
|
|
477
466
|
if (local_version) {
|
|
478
467
|
var r = await braid_blob.get(b, {
|
|
468
|
+
...options,
|
|
479
469
|
signal: ac.signal,
|
|
480
470
|
head: true,
|
|
481
471
|
dont_retry: true,
|
|
482
472
|
version: local_version,
|
|
483
|
-
headers: options.headers,
|
|
484
|
-
content_type: options.content_type,
|
|
485
|
-
peer: options.peer,
|
|
486
473
|
})
|
|
487
474
|
server_has_our_version = !!r
|
|
488
475
|
}
|
|
489
476
|
|
|
490
|
-
// Local -> remote
|
|
491
|
-
|
|
477
|
+
// Local -> remote
|
|
478
|
+
await braid_blob.get(a, {
|
|
479
|
+
...options,
|
|
492
480
|
signal: ac.signal,
|
|
493
|
-
|
|
494
|
-
content_type: options.content_type,
|
|
495
|
-
peer: options.peer,
|
|
481
|
+
parents: server_has_our_version ? local_version : null,
|
|
496
482
|
subscribe: async update => {
|
|
497
483
|
try {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
484
|
+
if (update.delete) {
|
|
485
|
+
var x = await braid_blob.delete(b, {
|
|
486
|
+
...options,
|
|
487
|
+
signal: ac.signal,
|
|
488
|
+
dont_retry: true,
|
|
489
|
+
content_type: update.content_type,
|
|
490
|
+
})
|
|
491
|
+
if (!x.ok) handle_error(new Error('failed to delete'))
|
|
492
|
+
} else {
|
|
493
|
+
var x = await braid_blob.put(b, update.body, {
|
|
494
|
+
...options,
|
|
495
|
+
signal: ac.signal,
|
|
496
|
+
dont_retry: true,
|
|
497
|
+
version: update.version,
|
|
498
|
+
content_type: update.content_type,
|
|
499
|
+
})
|
|
500
|
+
if ((x.status === 401 || x.status === 403) && options.on_unauthorized) {
|
|
501
|
+
await options.on_unauthorized?.()
|
|
502
|
+
} else if (!x.ok) handle_error(new Error('failed to PUT: ' + x.status))
|
|
503
|
+
}
|
|
510
504
|
} catch (e) {
|
|
511
|
-
if (e.name !== 'AbortError')
|
|
505
|
+
if (e.name !== 'AbortError')
|
|
506
|
+
handle_error(e)
|
|
512
507
|
}
|
|
513
508
|
}
|
|
514
|
-
}
|
|
515
|
-
// Only set parents if server already has our version
|
|
516
|
-
// If server doesn't have it, omit parents so subscription sends everything
|
|
517
|
-
if (server_has_our_version) {
|
|
518
|
-
a_ops.parents = local_version
|
|
519
|
-
}
|
|
509
|
+
})
|
|
520
510
|
|
|
521
|
-
// Remote -> local
|
|
522
|
-
var
|
|
511
|
+
// Remote -> local
|
|
512
|
+
var remote_res = await braid_blob.get(b, {
|
|
513
|
+
...options,
|
|
523
514
|
signal: ac.signal,
|
|
524
515
|
dont_retry: true,
|
|
525
|
-
|
|
526
|
-
content_type: options.content_type,
|
|
527
|
-
peer: options.peer,
|
|
516
|
+
parents: local_version,
|
|
528
517
|
subscribe: async update => {
|
|
529
|
-
await braid_blob.
|
|
518
|
+
if (update.delete) await braid_blob.delete(a, {
|
|
519
|
+
...options,
|
|
520
|
+
signal: ac.signal,
|
|
521
|
+
content_type: update.content_type,
|
|
522
|
+
})
|
|
523
|
+
else await braid_blob.put(a, update.body, {
|
|
524
|
+
...options,
|
|
525
|
+
signal: ac.signal,
|
|
530
526
|
version: update.version,
|
|
531
|
-
headers: options.headers,
|
|
532
527
|
content_type: update.content_type,
|
|
533
|
-
peer: options.peer,
|
|
534
528
|
})
|
|
535
|
-
remote_first_put()
|
|
536
529
|
},
|
|
537
530
|
on_error: e => {
|
|
538
531
|
options.on_disconnect?.()
|
|
539
532
|
handle_error(e)
|
|
540
533
|
}
|
|
541
|
-
}
|
|
542
|
-
// Use fork-point (parents) to avoid receiving data we already have
|
|
543
|
-
if (local_version) {
|
|
544
|
-
b_ops.parents = local_version
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// Set up both subscriptions, handling cases where one doesn't exist yet
|
|
548
|
-
braid_blob.get(a, a_ops).then(x =>
|
|
549
|
-
x || remote_first_put_promise.then(() =>
|
|
550
|
-
braid_blob.get(a, a_ops)))
|
|
551
|
-
|
|
552
|
-
var remote_res = await braid_blob.get(b, b_ops)
|
|
553
|
-
|
|
554
|
-
// If remote doesn't exist yet, wait for it to be created then reconnect
|
|
555
|
-
if (!remote_res) {
|
|
556
|
-
await local_first_put_promise
|
|
557
|
-
disconnect()
|
|
558
|
-
connect()
|
|
559
|
-
}
|
|
560
|
-
|
|
534
|
+
})
|
|
561
535
|
options.on_res?.(remote_res)
|
|
562
|
-
|
|
563
|
-
// Otherwise, on_error will call handle_error when connection drops
|
|
564
536
|
} catch (e) {
|
|
565
537
|
handle_error(e)
|
|
566
538
|
}
|
|
@@ -570,24 +542,55 @@ function create_braid_blob() {
|
|
|
570
542
|
}
|
|
571
543
|
|
|
572
544
|
function compare_events(a, b) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
var c = a_num.length - b_num.length
|
|
577
|
-
if (c) return c
|
|
545
|
+
if (!a) a = ''
|
|
546
|
+
if (!b) b = ''
|
|
578
547
|
|
|
579
|
-
var c =
|
|
548
|
+
var c = compare_seqs(get_event_seq(a), get_event_seq(b))
|
|
580
549
|
if (c) return c
|
|
581
550
|
|
|
582
|
-
|
|
551
|
+
if (a < b) return -1
|
|
552
|
+
if (a > b) return 1
|
|
553
|
+
return 0
|
|
583
554
|
}
|
|
584
555
|
|
|
585
556
|
function get_event_seq(e) {
|
|
557
|
+
if (!e) return ''
|
|
558
|
+
|
|
586
559
|
for (let i = e.length - 1; i >= 0; i--)
|
|
587
560
|
if (e[i] === '-') return e.slice(i + 1)
|
|
588
561
|
return e
|
|
589
562
|
}
|
|
590
563
|
|
|
564
|
+
function increment_seq(s) {
|
|
565
|
+
if (!s) return '1'
|
|
566
|
+
|
|
567
|
+
let last = s[s.length - 1]
|
|
568
|
+
let rest = s.slice(0, -1)
|
|
569
|
+
|
|
570
|
+
if (last >= '0' && last <= '8')
|
|
571
|
+
return rest + String.fromCharCode(last.charCodeAt(0) + 1)
|
|
572
|
+
else
|
|
573
|
+
return increment_seq(rest) + '0'
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function max_seq(a, b) {
|
|
577
|
+
if (!a) a = ''
|
|
578
|
+
if (!b) b = ''
|
|
579
|
+
|
|
580
|
+
if (compare_seqs(a, b) > 0) return a
|
|
581
|
+
return b
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function compare_seqs(a, b) {
|
|
585
|
+
if (!a) a = ''
|
|
586
|
+
if (!b) b = ''
|
|
587
|
+
|
|
588
|
+
if (a.length !== b.length) return a.length - b.length
|
|
589
|
+
if (a < b) return -1
|
|
590
|
+
if (a > b) return 1
|
|
591
|
+
return 0
|
|
592
|
+
}
|
|
593
|
+
|
|
591
594
|
function ascii_ify(s) {
|
|
592
595
|
return s.replace(/[^\x20-\x7E]/g, c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'))
|
|
593
596
|
}
|
package/package.json
CHANGED
package/test/tests.js
CHANGED
|
@@ -378,7 +378,7 @@ runTest(
|
|
|
378
378
|
|
|
379
379
|
return r.status
|
|
380
380
|
},
|
|
381
|
-
'
|
|
381
|
+
'200'
|
|
382
382
|
)
|
|
383
383
|
|
|
384
384
|
runTest(
|
|
@@ -431,45 +431,35 @@ runTest(
|
|
|
431
431
|
)
|
|
432
432
|
|
|
433
433
|
runTest(
|
|
434
|
-
"test
|
|
434
|
+
"test that aborting cleans up subscription",
|
|
435
435
|
async () => {
|
|
436
436
|
var r1 = await braid_fetch(`/eval`, {
|
|
437
437
|
method: 'POST',
|
|
438
438
|
body: `void (async () => {
|
|
439
|
-
var test_id = 'test-
|
|
440
|
-
var db_folder = __dirname + '/' + test_id + '-db'
|
|
441
|
-
var meta_folder = __dirname + '/' + test_id + '-meta'
|
|
439
|
+
var test_id = '/test-' + Math.random().toString(36).slice(2)
|
|
442
440
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
bb.meta_folder = meta_folder
|
|
441
|
+
// Put a file
|
|
442
|
+
await braid_blob.put(test_id, 'hello')
|
|
446
443
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
subscribe: (update) => { got_update = true }
|
|
455
|
-
})
|
|
444
|
+
// Subscribe to it
|
|
445
|
+
var got_update = false
|
|
446
|
+
var ac = new AbortController()
|
|
447
|
+
await braid_blob.get(test_id, {
|
|
448
|
+
signal: ac.signal,
|
|
449
|
+
subscribe: (update) => { got_update = true }
|
|
450
|
+
})
|
|
456
451
|
|
|
457
|
-
|
|
458
|
-
|
|
452
|
+
// Verify subscription exists
|
|
453
|
+
var has_sub_before = !!braid_blob.key_to_subs[test_id]
|
|
459
454
|
|
|
460
|
-
|
|
461
|
-
|
|
455
|
+
await new Promise(done => setTimeout(done, 30))
|
|
456
|
+
ac.abort()
|
|
457
|
+
await new Promise(done => setTimeout(done, 30))
|
|
462
458
|
|
|
463
|
-
|
|
464
|
-
|
|
459
|
+
// Verify subscription is cleaned up
|
|
460
|
+
var has_sub_after = !!braid_blob.key_to_subs[test_id]
|
|
465
461
|
|
|
466
|
-
|
|
467
|
-
} catch (e) {
|
|
468
|
-
res.end('error: ' + e.message)
|
|
469
|
-
} finally {
|
|
470
|
-
await require('fs').promises.rm(db_folder, { recursive: true, force: true })
|
|
471
|
-
await require('fs').promises.rm(meta_folder, { recursive: true, force: true })
|
|
472
|
-
}
|
|
462
|
+
res.end('' + (has_sub_before && !has_sub_after))
|
|
473
463
|
})()`
|
|
474
464
|
})
|
|
475
465
|
return await r1.text()
|
|
@@ -640,19 +630,6 @@ runTest(
|
|
|
640
630
|
'false'
|
|
641
631
|
)
|
|
642
632
|
|
|
643
|
-
runTest(
|
|
644
|
-
"test that subscribe sends 404 if there is no file.",
|
|
645
|
-
async () => {
|
|
646
|
-
var key = 'test-' + Math.random().toString(36).slice(2)
|
|
647
|
-
|
|
648
|
-
var r = await braid_fetch(`/${key}`, {
|
|
649
|
-
subscribe: true,
|
|
650
|
-
})
|
|
651
|
-
return r.status
|
|
652
|
-
},
|
|
653
|
-
'404'
|
|
654
|
-
)
|
|
655
|
-
|
|
656
633
|
runTest(
|
|
657
634
|
"test that we get 404 when file doesn't exist, on GET without subscribe.",
|
|
658
635
|
async () => {
|
|
@@ -1072,8 +1049,7 @@ runTest(
|
|
|
1072
1049
|
method: 'POST',
|
|
1073
1050
|
body: `void (async () => {
|
|
1074
1051
|
try {
|
|
1075
|
-
var
|
|
1076
|
-
var remote_url = new URL('http://localhost:' + req.socket.localPort + '${remote_key}')
|
|
1052
|
+
var remote_url = new URL('http://localhost:' + port + '${remote_key}')
|
|
1077
1053
|
|
|
1078
1054
|
// Start sync with URL as first argument (should swap internally)
|
|
1079
1055
|
braid_blob.sync(remote_url, '${local_key}')
|
|
@@ -1165,10 +1141,8 @@ runTest(
|
|
|
1165
1141
|
method: 'POST',
|
|
1166
1142
|
body: `void (async () => {
|
|
1167
1143
|
try {
|
|
1168
|
-
var braid_blob = require(\`\${__dirname}/../index.js\`)
|
|
1169
|
-
|
|
1170
1144
|
// Put locally with SAME version - so when sync connects, no updates need to flow
|
|
1171
|
-
await braid_blob.put('${local_key}',
|
|
1145
|
+
await braid_blob.put('${local_key}', 'same content', { version: ['same-version-123'] })
|
|
1172
1146
|
|
|
1173
1147
|
// Wrap db.read to count calls for our specific key
|
|
1174
1148
|
var read_count = 0
|
|
@@ -1178,7 +1152,7 @@ runTest(
|
|
|
1178
1152
|
return original_read.call(this, key)
|
|
1179
1153
|
}
|
|
1180
1154
|
|
|
1181
|
-
var remote_url = new URL('http://localhost:' +
|
|
1155
|
+
var remote_url = new URL('http://localhost:' + port + '/${remote_key}')
|
|
1182
1156
|
|
|
1183
1157
|
// Create an AbortController to stop the sync
|
|
1184
1158
|
var ac = new AbortController()
|
|
@@ -1305,7 +1279,6 @@ runTest(
|
|
|
1305
1279
|
// Try to subscribe with parents 200 (newer than what server has)
|
|
1306
1280
|
// This triggers the "unknown version" error which gets caught and returns 309
|
|
1307
1281
|
var r = await braid_fetch(`/${key}`, {
|
|
1308
|
-
subscribe: true,
|
|
1309
1282
|
parents: ['200']
|
|
1310
1283
|
})
|
|
1311
1284
|
|
|
@@ -1537,38 +1510,22 @@ runTest(
|
|
|
1537
1510
|
var r1 = await braid_fetch(`/eval`, {
|
|
1538
1511
|
method: 'POST',
|
|
1539
1512
|
body: `void (async () => {
|
|
1540
|
-
var
|
|
1541
|
-
var test_id = 'test-abort-get-' + Math.random().toString(36).slice(2)
|
|
1542
|
-
var db_folder = __dirname + '/' + test_id + '-db'
|
|
1543
|
-
var meta_folder = __dirname + '/' + test_id + '-meta'
|
|
1544
|
-
|
|
1545
|
-
try {
|
|
1546
|
-
var bb = braid_blob.create_braid_blob()
|
|
1547
|
-
bb.db_folder = db_folder
|
|
1548
|
-
bb.meta_folder = meta_folder
|
|
1513
|
+
var test_id = '/test-abort-get-' + Math.random().toString(36).slice(2)
|
|
1549
1514
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1515
|
+
// Put a file first
|
|
1516
|
+
await braid_blob.put(test_id, 'hello', { version: ['1'] })
|
|
1552
1517
|
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1518
|
+
// Create an already-aborted signal
|
|
1519
|
+
var ac = new AbortController()
|
|
1520
|
+
ac.abort()
|
|
1556
1521
|
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
header_cb: () => { header_called = true }
|
|
1562
|
-
})
|
|
1522
|
+
// Try to get with aborted signal (after header_cb)
|
|
1523
|
+
var result = await braid_blob.get(test_id, {
|
|
1524
|
+
signal: ac.signal,
|
|
1525
|
+
})
|
|
1563
1526
|
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
} catch (e) {
|
|
1567
|
-
res.end('error: ' + e.message)
|
|
1568
|
-
} finally {
|
|
1569
|
-
await fs.rm(db_folder, { recursive: true, force: true })
|
|
1570
|
-
await fs.rm(meta_folder, { recursive: true, force: true })
|
|
1571
|
-
}
|
|
1527
|
+
// Result should be undefined since operation was aborted already
|
|
1528
|
+
res.end(result === undefined ? 'aborted' : 'not aborted')
|
|
1572
1529
|
})()`
|
|
1573
1530
|
})
|
|
1574
1531
|
return await r1.text()
|