braid-blob 0.0.14 → 0.0.16
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/LICENSE.txt +11 -0
- package/README.md +83 -1
- package/blob.png +0 -0
- package/index.js +217 -181
- package/package.json +1 -1
- package/plop.png +0 -0
- package/test/server.js +0 -1
- package/test/test.html +220 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Invisible Property License
|
|
2
|
+
|
|
3
|
+
1. You have the right to use this IP for any purpose.
|
|
4
|
+
2. If you make profit, you agree to give back a fair share of the profit to
|
|
5
|
+
the creators of this IP.
|
|
6
|
+
3. The creators will tell you how much they think is a fair share, if your
|
|
7
|
+
usage matters to them, and promise not to take you to court to enforce
|
|
8
|
+
the agreement.
|
|
9
|
+
|
|
10
|
+
(In other words, this license thus runs on the honor system. You are invited
|
|
11
|
+
to participate in our community with honor.)
|
package/README.md
CHANGED
|
@@ -1,5 +1,87 @@
|
|
|
1
1
|
# braid-blob
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
A simple, self-contained library for synchronizing binary blobs (files, images, etc.) over HTTP using [Braid-HTTP](https://braid.org). It provides real-time synchronization with last-write-wins (LWW) conflict resolution and persistent storage.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install braid-blob
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Basic Server
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
var braid_blob = require('braid-blob')
|
|
17
|
+
|
|
18
|
+
require('http').createServer((req, res) => {
|
|
19
|
+
braid_blob.serve(req, res)
|
|
20
|
+
}).listen(8888)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
That's it! You now have a blob synchronization server.
|
|
24
|
+
|
|
25
|
+
### Usage Examples
|
|
26
|
+
|
|
27
|
+
First let's upload a file:
|
|
28
|
+
```bash
|
|
29
|
+
curl -X PUT -H "Content-Type: image/png" -T blob.png http://localhost:8888/image.png
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
View image in browser at http://localhost:8888/image.png
|
|
33
|
+
|
|
34
|
+
To see updates, let's do a textual example for easy viewing:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
curl -X PUT -H "Content-Type: text/plain" -d "hello" http://localhost:8888/text
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Next, subscribe for updates:
|
|
41
|
+
```
|
|
42
|
+
curl -H "Subscribe: true" http://localhost:8888/text
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Now, in another terminal, write over the file:
|
|
46
|
+
```bash
|
|
47
|
+
curl -X PUT -H "Content-Type: text/plain" -d "world" http://localhost:8888/text
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Should see activity in the first terminal showing the update.
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
# Delete a file
|
|
54
|
+
curl -X DELETE http://localhost:8888/text
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## API
|
|
58
|
+
|
|
59
|
+
### Configuration
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
var braid_blob = require('braid-blob')
|
|
63
|
+
|
|
64
|
+
// Set custom storage location (default: './braid-blob-db')
|
|
65
|
+
braid_blob.db_folder = './custom_files_folder'
|
|
66
|
+
|
|
67
|
+
// Set custom peer ID (default: auto-generated and persisted)
|
|
68
|
+
braid_blob.peer = 'my-server-id'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `braid_blob.serve(req, res, options)`
|
|
72
|
+
|
|
73
|
+
Handles HTTP requests for blob storage and synchronization.
|
|
74
|
+
|
|
75
|
+
**Parameters:**
|
|
76
|
+
- `req` - HTTP request object
|
|
77
|
+
- `res` - HTTP response object
|
|
78
|
+
- `options` - Optional configuration object
|
|
79
|
+
- `key` - Override the resource key (default: URL path)
|
|
80
|
+
|
|
81
|
+
**Supported HTTP Methods:**
|
|
82
|
+
- `GET` - Retrieve a blob (with optional `Subscribe: true` header)
|
|
83
|
+
- `PUT` - Store/update a blob
|
|
84
|
+
- `DELETE` - Remove a blob
|
|
3
85
|
|
|
4
86
|
## Testing
|
|
5
87
|
|
package/blob.png
ADDED
|
Binary file
|
package/index.js
CHANGED
|
@@ -1,208 +1,244 @@
|
|
|
1
|
-
|
|
2
1
|
var {http_server: braidify, free_cors} = require('braid-http'),
|
|
3
2
|
fs = require('fs'),
|
|
4
3
|
path = require('path')
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
5
|
+
function create_braid_blob() {
|
|
6
|
+
var braid_blob = {
|
|
7
|
+
db_folder: './braid-blob-db',
|
|
8
|
+
cache: {},
|
|
9
|
+
key_to_subs: {},
|
|
10
|
+
peer: null // we'll try to load this from a file, if not set by the user
|
|
11
|
+
}
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
braid_blob.init = async () => {
|
|
14
|
+
braid_blob.init = () => {}
|
|
13
15
|
|
|
14
|
-
braid_blob.
|
|
15
|
-
|
|
16
|
+
await fs.promises.mkdir(`${braid_blob.db_folder}/blob`, { recursive: true })
|
|
17
|
+
await fs.promises.mkdir(`${braid_blob.db_folder}/meta`, { recursive: true })
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
// establish a peer id
|
|
20
|
+
if (!braid_blob.peer)
|
|
21
|
+
try {
|
|
22
|
+
braid_blob.peer = await fs.promises.readFile(`${braid_blob.db_folder}/peer.txt`, 'utf8')
|
|
23
|
+
} catch (e) {}
|
|
24
|
+
if (!braid_blob.peer)
|
|
25
|
+
braid_blob.peer = Math.random().toString(36).slice(2)
|
|
26
|
+
await fs.promises.writeFile(`${braid_blob.db_folder}/peer.txt`, braid_blob.peer)
|
|
27
|
+
}
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
29
|
+
braid_blob.serve = async (req, res, options = {}) => {
|
|
30
|
+
await braid_blob.init()
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
var body = req.method === 'PUT' && await slurp(req)
|
|
32
|
+
if (!options.key) options.key = decodeURIComponent(req.url.split('?')[0])
|
|
25
33
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const metaname = `${braid_blob.meta_folder}/${encode_filename(options.key)}`
|
|
29
|
-
|
|
30
|
-
// Read the meta file
|
|
31
|
-
var meta = {}
|
|
32
|
-
try {
|
|
33
|
-
meta = JSON.parse(await fs.promises.readFile(metaname, 'utf8'))
|
|
34
|
-
} catch (e) {}
|
|
35
|
-
var our_v = meta.version
|
|
36
|
-
|
|
37
|
-
if (req.method === 'GET') {
|
|
38
|
-
// Handle GET request for binary files
|
|
39
|
-
|
|
40
|
-
if (our_v == null) {
|
|
41
|
-
res.statusCode = 404
|
|
42
|
-
res.setHeader('Content-Type', 'text/plain')
|
|
43
|
-
return res.end('File Not Found')
|
|
44
|
-
}
|
|
34
|
+
braidify(req, res)
|
|
35
|
+
if (res.is_multiplexer) return
|
|
45
36
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
res.statusCode = 406
|
|
49
|
-
res.setHeader('Content-Type', 'text/plain')
|
|
50
|
-
return res.end(`Content-Type of ${meta.content_type} not in Accept: ${req.headers.accept}`)
|
|
51
|
-
}
|
|
37
|
+
// Handle OPTIONS request
|
|
38
|
+
if (req.method === 'OPTIONS') return res.end();
|
|
52
39
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (!req.subscribe)
|
|
63
|
-
return res.end(await fs.promises.readFile(filename))
|
|
64
|
-
|
|
65
|
-
if (!res.hasHeader("editable"))
|
|
66
|
-
res.setHeader("Editable", "true")
|
|
67
|
-
|
|
68
|
-
// Start a subscription for future updates.
|
|
69
|
-
if (!key_to_subs[options.key]) key_to_subs[options.key] = new Map()
|
|
70
|
-
var peer = req.peer || Math.random().toString(36).slice(2)
|
|
71
|
-
key_to_subs[options.key].set(peer, res)
|
|
72
|
-
|
|
73
|
-
res.startSubscription({ onClose: () => {
|
|
74
|
-
key_to_subs[options.key].delete(peer)
|
|
75
|
-
if (!key_to_subs[options.key].size)
|
|
76
|
-
delete key_to_subs[options.key]
|
|
77
|
-
}})
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// Send an immediate update when:
|
|
81
|
-
if (!req.parents || // 1) They have no version history
|
|
82
|
-
// (need full sync)
|
|
83
|
-
!req.parents.length || // 2) Or their version is the empty set
|
|
84
|
-
our_v > 1*req.parents[0] // 3) Or our version is newer
|
|
85
|
-
)
|
|
86
|
-
return res.sendUpdate({
|
|
87
|
-
version: ['' + our_v],
|
|
88
|
-
'Merge-Type': 'lww',
|
|
89
|
-
body: await fs.promises.readFile(filename)
|
|
90
|
-
})
|
|
91
|
-
else res.write('\n\n') // get the node http code to send headers
|
|
92
|
-
} else if (req.method === 'PUT') {
|
|
93
|
-
// Handle PUT request to update binary files
|
|
94
|
-
|
|
95
|
-
// Ensure directory exists
|
|
96
|
-
await fs.promises.mkdir(path.dirname(filename), { recursive: true })
|
|
97
|
-
await fs.promises.mkdir(path.dirname(metaname), { recursive: true })
|
|
98
|
-
|
|
99
|
-
var their_v =
|
|
100
|
-
!req.version ?
|
|
101
|
-
// we'll give them a version in this case
|
|
102
|
-
Math.max(our_v != null ? our_v + 1 : 0, Date.now()) :
|
|
103
|
-
!req.version.length ?
|
|
104
|
-
null :
|
|
105
|
-
1*req.version[0]
|
|
106
|
-
|
|
107
|
-
if (their_v != null &&
|
|
108
|
-
(our_v == null || their_v > our_v)) {
|
|
109
|
-
|
|
110
|
-
// Write the file
|
|
111
|
-
await fs.promises.writeFile(filename, body)
|
|
112
|
-
|
|
113
|
-
// Write the meta file
|
|
114
|
-
meta.version = their_v
|
|
115
|
-
if (req.headers['content-type'])
|
|
116
|
-
meta.content_type = req.headers['content-type']
|
|
117
|
-
await fs.promises.writeFile(metaname, JSON.stringify(meta))
|
|
118
|
-
|
|
119
|
-
// Notify all subscriptions of the update
|
|
120
|
-
// (except the peer which made the PUT request itself)
|
|
121
|
-
if (key_to_subs[options.key])
|
|
122
|
-
for (var [peer, sub] of key_to_subs[options.key].entries())
|
|
123
|
-
if (peer !== req.peer)
|
|
124
|
-
sub.sendUpdate({
|
|
125
|
-
version: ['' + their_v],
|
|
126
|
-
'Merge-Type': 'lww',
|
|
127
|
-
body
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
res.setHeader("Version", `"${their_v}"`)
|
|
131
|
-
} else {
|
|
132
|
-
res.setHeader("Version", our_v != null ? `"${our_v}"` : '')
|
|
133
|
-
}
|
|
134
|
-
res.end('')
|
|
135
|
-
} else if (req.method === 'DELETE') {
|
|
136
|
-
try {
|
|
137
|
-
await fs.promises.unlink(filename)
|
|
138
|
-
} catch (e) {}
|
|
40
|
+
// consume PUT body
|
|
41
|
+
var body = req.method === 'PUT' && await slurp(req)
|
|
42
|
+
|
|
43
|
+
await within_fiber(options.key, async () => {
|
|
44
|
+
const filename = `${braid_blob.db_folder}/blob/${encode_filename(options.key)}`
|
|
45
|
+
const metaname = `${braid_blob.db_folder}/meta/${encode_filename(options.key)}`
|
|
46
|
+
|
|
47
|
+
// Read the meta file
|
|
48
|
+
var meta = {}
|
|
139
49
|
try {
|
|
140
|
-
await fs.promises.
|
|
50
|
+
meta = JSON.parse(await fs.promises.readFile(metaname, 'utf8'))
|
|
141
51
|
} catch (e) {}
|
|
142
|
-
res.statusCode = 204 // No Content
|
|
143
|
-
res.end('')
|
|
144
|
-
}
|
|
145
|
-
})
|
|
146
|
-
}
|
|
147
52
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
53
|
+
if (req.method === 'GET') {
|
|
54
|
+
// Handle GET request for binary files
|
|
55
|
+
|
|
56
|
+
if (meta.event == null) {
|
|
57
|
+
res.statusCode = 404
|
|
58
|
+
res.setHeader('Content-Type', 'text/plain')
|
|
59
|
+
return res.end('File Not Found')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (meta.content_type && req.headers.accept &&
|
|
63
|
+
!isAcceptable(meta.content_type, req.headers.accept)) {
|
|
64
|
+
res.statusCode = 406
|
|
65
|
+
res.setHeader('Content-Type', 'text/plain')
|
|
66
|
+
return res.end(`Content-Type of ${meta.content_type} not in Accept: ${req.headers.accept}`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Set Version header;
|
|
70
|
+
// but if this is a subscription,
|
|
71
|
+
// then we set Current-Version instead
|
|
72
|
+
res.setHeader((req.subscribe ? 'Current-' : '') + 'Version',
|
|
73
|
+
JSON.stringify(meta.event))
|
|
74
|
+
|
|
75
|
+
// Set Content-Type
|
|
76
|
+
if (meta.content_type)
|
|
77
|
+
res.setHeader('Content-Type', meta.content_type)
|
|
78
|
+
|
|
79
|
+
if (!req.subscribe)
|
|
80
|
+
return res.end(await fs.promises.readFile(filename))
|
|
81
|
+
|
|
82
|
+
if (!res.hasHeader("editable"))
|
|
83
|
+
res.setHeader("Editable", "true")
|
|
84
|
+
|
|
85
|
+
// Start a subscription for future updates.
|
|
86
|
+
if (!braid_blob.key_to_subs[options.key])
|
|
87
|
+
braid_blob.key_to_subs[options.key] = new Map()
|
|
88
|
+
var peer = req.peer || Math.random().toString(36).slice(2)
|
|
89
|
+
braid_blob.key_to_subs[options.key].set(peer, res)
|
|
90
|
+
|
|
91
|
+
res.startSubscription({ onClose: () => {
|
|
92
|
+
braid_blob.key_to_subs[options.key].delete(peer)
|
|
93
|
+
if (!braid_blob.key_to_subs[options.key].size)
|
|
94
|
+
delete braid_blob.key_to_subs[options.key]
|
|
95
|
+
}})
|
|
96
|
+
|
|
97
|
+
// Send an immediate update when:
|
|
98
|
+
if (!req.parents || // 1) They want everything,
|
|
99
|
+
!req.parents.length || // 2) Or everything past the empty set,
|
|
100
|
+
compare_events(meta.event, req.parents[0]) > 0
|
|
101
|
+
// 3) Or what we have is newer
|
|
102
|
+
)
|
|
103
|
+
return res.sendUpdate({
|
|
104
|
+
version: [meta.event],
|
|
105
|
+
'Merge-Type': 'lww',
|
|
106
|
+
body: await fs.promises.readFile(filename)
|
|
107
|
+
})
|
|
108
|
+
else res.write('\n\n') // get the node http code to send headers
|
|
109
|
+
} else if (req.method === 'PUT') {
|
|
110
|
+
// Handle PUT request to update binary files
|
|
111
|
+
|
|
112
|
+
var their_e =
|
|
113
|
+
!req.version ?
|
|
114
|
+
// we'll give them a event id in this case
|
|
115
|
+
`${braid_blob.peer}-${Math.max(Date.now(),
|
|
116
|
+
meta.event ? 1*get_event_seq(meta.event) + 1 : -Infinity)}` :
|
|
117
|
+
!req.version.length ?
|
|
118
|
+
null :
|
|
119
|
+
req.version[0]
|
|
120
|
+
|
|
121
|
+
if (their_e != null &&
|
|
122
|
+
(meta.event == null ||
|
|
123
|
+
compare_events(their_e, meta.event) > 0)) {
|
|
124
|
+
meta.event = their_e
|
|
125
|
+
|
|
126
|
+
// Write the file
|
|
127
|
+
await fs.promises.writeFile(filename, body)
|
|
128
|
+
|
|
129
|
+
// Write the meta file
|
|
130
|
+
if (req.headers['content-type'])
|
|
131
|
+
meta.content_type = req.headers['content-type']
|
|
132
|
+
await fs.promises.writeFile(metaname, JSON.stringify(meta))
|
|
133
|
+
|
|
134
|
+
// Notify all subscriptions of the update
|
|
135
|
+
// (except the peer which made the PUT request itself)
|
|
136
|
+
if (braid_blob.key_to_subs[options.key])
|
|
137
|
+
for (var [peer, sub] of braid_blob.key_to_subs[options.key].entries())
|
|
138
|
+
if (peer !== req.peer)
|
|
139
|
+
sub.sendUpdate({
|
|
140
|
+
version: [meta.event],
|
|
141
|
+
'Merge-Type': 'lww',
|
|
142
|
+
body
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
res.setHeader("Version", meta.event != null ? JSON.stringify(meta.event) : '')
|
|
146
|
+
res.end('')
|
|
147
|
+
} else if (req.method === 'DELETE') {
|
|
148
|
+
try {
|
|
149
|
+
await fs.promises.unlink(filename)
|
|
150
|
+
} catch (e) {}
|
|
151
|
+
try {
|
|
152
|
+
await fs.promises.unlink(metaname)
|
|
153
|
+
} catch (e) {}
|
|
154
|
+
res.statusCode = 204 // No Content
|
|
155
|
+
res.end('')
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
}
|
|
151
159
|
|
|
152
|
-
|
|
153
|
-
|
|
160
|
+
function compare_events(a, b) {
|
|
161
|
+
var a_num = get_event_seq(a)
|
|
162
|
+
var b_num = get_event_seq(b)
|
|
154
163
|
|
|
155
|
-
|
|
156
|
-
|
|
164
|
+
var c = a_num.length - b_num.length
|
|
165
|
+
if (c) return c
|
|
157
166
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
var prev = within_fiber.chains[id] || Promise.resolve()
|
|
161
|
-
var curr = prev.then(async () => {
|
|
162
|
-
try {
|
|
163
|
-
return await func()
|
|
164
|
-
} finally {
|
|
165
|
-
if (within_fiber.chains[id] === curr)
|
|
166
|
-
delete within_fiber.chains[id]
|
|
167
|
-
}
|
|
168
|
-
})
|
|
169
|
-
return within_fiber.chains[id] = curr
|
|
170
|
-
}
|
|
167
|
+
var c = a_num.localeCompare(b_num)
|
|
168
|
+
if (c) return c
|
|
171
169
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
170
|
+
return a.localeCompare(b)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function get_event_seq(e) {
|
|
174
|
+
for (let i = e.length - 1; i >= 0; i--)
|
|
175
|
+
if (e[i] === '-') return e.slice(i + 1)
|
|
176
|
+
return e
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function encode_filename(filename) {
|
|
180
|
+
// Swap all "!" and "/" characters
|
|
181
|
+
let swapped = filename.replace(/[!/]/g, (match) => (match === "!" ? "/" : "!"))
|
|
182
|
+
|
|
183
|
+
// Encode the filename using encodeURIComponent()
|
|
184
|
+
let encoded = encodeURIComponent(swapped)
|
|
185
|
+
|
|
186
|
+
return encoded
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function within_fiber(id, func) {
|
|
190
|
+
if (!within_fiber.chains) within_fiber.chains = {}
|
|
191
|
+
var prev = within_fiber.chains[id] || Promise.resolve()
|
|
192
|
+
var curr = prev.then(async () => {
|
|
193
|
+
try {
|
|
194
|
+
return await func()
|
|
195
|
+
} finally {
|
|
196
|
+
if (within_fiber.chains[id] === curr)
|
|
197
|
+
delete within_fiber.chains[id]
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
return within_fiber.chains[id] = curr
|
|
201
|
+
}
|
|
179
202
|
|
|
180
|
-
function
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
203
|
+
async function slurp(req) {
|
|
204
|
+
return await new Promise(done => {
|
|
205
|
+
var chunks = []
|
|
206
|
+
req.on('data', chunk => chunks.push(chunk))
|
|
207
|
+
req.on('end', () => done(Buffer.concat(chunks)))
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function isAcceptable(contentType, acceptHeader) {
|
|
212
|
+
// If no Accept header or Accept is */*, accept everything
|
|
213
|
+
if (!acceptHeader || acceptHeader === '*/*') return true;
|
|
190
214
|
|
|
191
|
-
//
|
|
192
|
-
|
|
215
|
+
// Parse the Accept header into individual media types
|
|
216
|
+
const acceptTypes = acceptHeader.split(',').map(type => type.trim());
|
|
193
217
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
218
|
+
for (const acceptType of acceptTypes) {
|
|
219
|
+
// Remove quality values (e.g., "text/html;q=0.9" -> "text/html")
|
|
220
|
+
const cleanAcceptType = acceptType.split(';')[0].trim();
|
|
221
|
+
|
|
222
|
+
// Exact match
|
|
223
|
+
if (cleanAcceptType === contentType) return true;
|
|
224
|
+
|
|
225
|
+
// Wildcard subtype match (e.g., "image/*" matches "image/png")
|
|
226
|
+
if (cleanAcceptType.endsWith('/*')) {
|
|
227
|
+
const acceptMain = cleanAcceptType.slice(0, -2);
|
|
228
|
+
const contentMain = contentType.split('/')[0];
|
|
229
|
+
if (acceptMain === contentMain) return true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Full wildcard
|
|
233
|
+
if (cleanAcceptType === '*/*') return true;
|
|
199
234
|
}
|
|
200
235
|
|
|
201
|
-
|
|
202
|
-
if (cleanAcceptType === '*/*') return true;
|
|
236
|
+
return false;
|
|
203
237
|
}
|
|
204
|
-
|
|
205
|
-
|
|
238
|
+
|
|
239
|
+
braid_blob.create_braid_blob = create_braid_blob
|
|
240
|
+
|
|
241
|
+
return braid_blob
|
|
206
242
|
}
|
|
207
243
|
|
|
208
|
-
module.exports =
|
|
244
|
+
module.exports = create_braid_blob()
|
package/package.json
CHANGED
package/plop.png
ADDED
|
Binary file
|
package/test/server.js
CHANGED
|
@@ -4,7 +4,6 @@ var port = process.argv[2] || 8889
|
|
|
4
4
|
var braid_blob = require(`${__dirname}/../index.js`)
|
|
5
5
|
var {free_cors} = require("braid-http")
|
|
6
6
|
braid_blob.db_folder = `${__dirname}/test_db_folder`
|
|
7
|
-
braid_blob.meta_folder = `${__dirname}/test_meta_folder`
|
|
8
7
|
|
|
9
8
|
var server = require("http").createServer(async (req, res) => {
|
|
10
9
|
console.log(`${req.method} ${req.url}`)
|
package/test/test.html
CHANGED
|
@@ -89,6 +89,152 @@ async function runTest(testName, testFunction, expectedResult) {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
runTest(
|
|
93
|
+
"test that peer.txt gets initialized on a fresh run",
|
|
94
|
+
async () => {
|
|
95
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
body: `void (async () => {
|
|
98
|
+
var db_name = 'test-db-' + Math.random().toString(36).slice(2)
|
|
99
|
+
|
|
100
|
+
var new_bb = braid_blob.create_braid_blob()
|
|
101
|
+
new_bb.db_folder = __dirname + '/' + db_name
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
await new_bb.serve({}, {})
|
|
105
|
+
} catch (e) {}
|
|
106
|
+
|
|
107
|
+
await require('fs').promises.rm(new_bb.db_folder,
|
|
108
|
+
{ recursive: true, force: true })
|
|
109
|
+
|
|
110
|
+
res.end(new_bb.peer)
|
|
111
|
+
|
|
112
|
+
})()`
|
|
113
|
+
})
|
|
114
|
+
return '' + ((await r1.text()).length > 5)
|
|
115
|
+
},
|
|
116
|
+
'true'
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
runTest(
|
|
120
|
+
"test that peer is same the second time we run from same db folder",
|
|
121
|
+
async () => {
|
|
122
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
body: `void (async () => {
|
|
125
|
+
var db = __dirname + '/test-db-' + Math.random().toString(36).slice(2)
|
|
126
|
+
|
|
127
|
+
var bb1 = braid_blob.create_braid_blob()
|
|
128
|
+
bb1.db_folder = db
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await bb1.serve({}, {})
|
|
132
|
+
} catch (e) {}
|
|
133
|
+
|
|
134
|
+
var bb2 = braid_blob.create_braid_blob()
|
|
135
|
+
bb2.db_folder = db
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
await bb2.serve({}, {})
|
|
139
|
+
} catch (e) {}
|
|
140
|
+
|
|
141
|
+
await require('fs').promises.rm(db, { recursive: true, force: true })
|
|
142
|
+
|
|
143
|
+
res.end('' + (bb1.peer === bb2.peer))
|
|
144
|
+
})()`
|
|
145
|
+
})
|
|
146
|
+
return await r1.text()
|
|
147
|
+
},
|
|
148
|
+
'true'
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
runTest(
|
|
152
|
+
"test that we can set the peer of a braid_blob object",
|
|
153
|
+
async () => {
|
|
154
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
body: `void (async () => {
|
|
157
|
+
var db = __dirname + '/test-db-' + Math.random().toString(36).slice(2)
|
|
158
|
+
|
|
159
|
+
var bb1 = braid_blob.create_braid_blob()
|
|
160
|
+
bb1.db_folder = db
|
|
161
|
+
bb1.peer = 'test_peer'
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
await bb1.serve({}, {})
|
|
165
|
+
} catch (e) {}
|
|
166
|
+
|
|
167
|
+
await require('fs').promises.rm(db, { recursive: true, force: true })
|
|
168
|
+
|
|
169
|
+
res.end(bb1.peer)
|
|
170
|
+
})()`
|
|
171
|
+
})
|
|
172
|
+
return await r1.text()
|
|
173
|
+
},
|
|
174
|
+
'test_peer'
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
runTest(
|
|
178
|
+
"test that PUTing with shorter event id doesn't do anything.",
|
|
179
|
+
async () => {
|
|
180
|
+
var key = 'test-' + Math.random().toString(36).slice(2)
|
|
181
|
+
|
|
182
|
+
var r = await braid_fetch(`/${key}`, {
|
|
183
|
+
method: 'PUT',
|
|
184
|
+
version: ['11'],
|
|
185
|
+
body: 'xyz'
|
|
186
|
+
})
|
|
187
|
+
if (!r.ok) throw 'got: ' + r.statusCode
|
|
188
|
+
|
|
189
|
+
var r = await braid_fetch(`/${key}`, {
|
|
190
|
+
method: 'PUT',
|
|
191
|
+
version: ['9'],
|
|
192
|
+
body: 'abc'
|
|
193
|
+
})
|
|
194
|
+
if (!r.ok) throw 'got: ' + r.statusCode
|
|
195
|
+
|
|
196
|
+
var r = await braid_fetch(`/${key}`)
|
|
197
|
+
return await r.text()
|
|
198
|
+
},
|
|
199
|
+
'xyz'
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
runTest(
|
|
203
|
+
"test that we ignore stuff after the ? in a url",
|
|
204
|
+
async () => {
|
|
205
|
+
var key = 'test-' + Math.random().toString(36).slice(2)
|
|
206
|
+
|
|
207
|
+
var r = await braid_fetch(`/${key}?blah`, {
|
|
208
|
+
method: 'PUT',
|
|
209
|
+
version: ['11'],
|
|
210
|
+
body: 'yo!'
|
|
211
|
+
})
|
|
212
|
+
if (!r.ok) throw 'got: ' + r.statusCode
|
|
213
|
+
|
|
214
|
+
var r = await braid_fetch(`/${key}`)
|
|
215
|
+
return await r.text()
|
|
216
|
+
},
|
|
217
|
+
'yo!'
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
runTest(
|
|
221
|
+
"test that we ignore stuff after the # in a url",
|
|
222
|
+
async () => {
|
|
223
|
+
var key = 'test-' + Math.random().toString(36).slice(2)
|
|
224
|
+
|
|
225
|
+
var r = await braid_fetch(`/${key}#blah?bloop`, {
|
|
226
|
+
method: 'PUT',
|
|
227
|
+
version: ['11'],
|
|
228
|
+
body: 'hi!'
|
|
229
|
+
})
|
|
230
|
+
if (!r.ok) throw 'got: ' + r.statusCode
|
|
231
|
+
|
|
232
|
+
var r = await braid_fetch(`/${key}`)
|
|
233
|
+
return await r.text()
|
|
234
|
+
},
|
|
235
|
+
'hi!'
|
|
236
|
+
)
|
|
237
|
+
|
|
92
238
|
runTest(
|
|
93
239
|
"test send an update to another peer",
|
|
94
240
|
async () => {
|
|
@@ -130,6 +276,80 @@ runTest(
|
|
|
130
276
|
'abc'
|
|
131
277
|
)
|
|
132
278
|
|
|
279
|
+
runTest(
|
|
280
|
+
"test having multiple subs",
|
|
281
|
+
async () => {
|
|
282
|
+
var key = 'test-' + Math.random().toString(36).slice(2)
|
|
283
|
+
var key2 = 'test2-' + Math.random().toString(36).slice(2)
|
|
284
|
+
|
|
285
|
+
var r = await braid_fetch(`/${key}`, {
|
|
286
|
+
method: 'PUT',
|
|
287
|
+
headers: {'Content-Type': 'text/plain'},
|
|
288
|
+
version: ['1'],
|
|
289
|
+
body: 'xyz'
|
|
290
|
+
})
|
|
291
|
+
if (!r.ok) throw 'got: ' + r.statusCode
|
|
292
|
+
|
|
293
|
+
var a = new AbortController()
|
|
294
|
+
var r = await braid_fetch(`/${key}`, {
|
|
295
|
+
signal: a.signal,
|
|
296
|
+
subscribe: true,
|
|
297
|
+
peer: key
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
var p = new Promise(done => {
|
|
301
|
+
r.subscribe(update => {
|
|
302
|
+
if (update.version?.[0] !== '2') return
|
|
303
|
+
done(update.body_text)
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
var r = await braid_fetch(`/${key2}`, {
|
|
308
|
+
method: 'PUT',
|
|
309
|
+
headers: {'Content-Type': 'text/plain'},
|
|
310
|
+
version: ['1'],
|
|
311
|
+
body: 'xyz2'
|
|
312
|
+
})
|
|
313
|
+
if (!r.ok) throw 'got: ' + r.statusCode
|
|
314
|
+
|
|
315
|
+
var a = new AbortController()
|
|
316
|
+
var r = await braid_fetch(`/${key2}`, {
|
|
317
|
+
signal: a.signal,
|
|
318
|
+
subscribe: true,
|
|
319
|
+
peer: key2
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
var p2 = new Promise(done => {
|
|
323
|
+
r.subscribe(update => {
|
|
324
|
+
if (update.version?.[0] !== '2') return
|
|
325
|
+
done(update.body_text)
|
|
326
|
+
})
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
var r = await braid_fetch(`/${key}`, {
|
|
330
|
+
method: 'PUT',
|
|
331
|
+
headers: {'Content-Type': 'text/plain'},
|
|
332
|
+
version: ['2'],
|
|
333
|
+
body: 'abc'
|
|
334
|
+
})
|
|
335
|
+
if (!r.ok) throw 'got: ' + r.statusCode
|
|
336
|
+
|
|
337
|
+
var r = await braid_fetch(`/${key2}`, {
|
|
338
|
+
method: 'PUT',
|
|
339
|
+
headers: {'Content-Type': 'text/plain'},
|
|
340
|
+
version: ['2'],
|
|
341
|
+
body: 'abc2'
|
|
342
|
+
})
|
|
343
|
+
if (!r.ok) throw 'got: ' + r.statusCode
|
|
344
|
+
|
|
345
|
+
var ret = await Promise.all([p, p2])
|
|
346
|
+
a.abort()
|
|
347
|
+
return 'got: ' + ret
|
|
348
|
+
|
|
349
|
+
},
|
|
350
|
+
'got: abc,abc2'
|
|
351
|
+
)
|
|
352
|
+
|
|
133
353
|
runTest(
|
|
134
354
|
"test getting a 406",
|
|
135
355
|
async () => {
|