braid-blob 0.0.53 → 0.0.55

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/README.md CHANGED
@@ -20,39 +20,27 @@ require('http').createServer((req, res) => {
20
20
  }).listen(8888)
21
21
  ```
22
22
 
23
- That's it! You now have a blob synchronization server.
23
+ That's it! You now have a blob synchronization server. Upload an image:
24
24
 
25
- ### Usage Examples
26
-
27
- First let's upload a file:
28
25
  ```bash
29
- curl -X PUT -H "Content-Type: image/png" -T blob.png http://localhost:8888/image.png
26
+ curl -X PUT -H "Content-Type: image/png" -T blob.png http://localhost:8888/blob.png
30
27
  ```
31
28
 
32
- View image in browser at http://localhost:8888/image.png
29
+ Then view it at http://localhost:8888/blob.png
33
30
 
34
- To see updates, let's do a textual example for easy viewing:
31
+ ### Demo
35
32
 
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
- ```
33
+ Run the demo server:
44
34
 
45
- Now, in another terminal, write over the file:
46
35
  ```bash
47
- curl -X PUT -H "Content-Type: text/plain" -d "world" http://localhost:8888/text
36
+ node server-demo.js
48
37
  ```
49
38
 
50
- Should see activity in the first terminal showing the update.
39
+ Then open http://localhost:8888 in your browser. You can drag and drop images to upload them, and open multiple browser windows to see real-time sync in action.
51
40
 
52
- ```
53
- # Delete a file
54
- curl -X DELETE http://localhost:8888/text
55
- ```
41
+ <!-- TODO: Add demo video
42
+ ![Demo](demo.mp4)
43
+ -->
56
44
 
57
45
  ## API
58
46
 
@@ -61,16 +49,8 @@ curl -X DELETE http://localhost:8888/text
61
49
  ```javascript
62
50
  var braid_blob = require('braid-blob')
63
51
 
64
- // Set custom blob storage location (default: './braid-blob-db')
65
- // This uses url-file-db for efficient URL-to-file mapping
66
- braid_blob.db_folder = './custom_files_folder'
67
-
68
- // Set custom metadata storage location (default: './braid-blob-meta')
69
- // Stores version metadata and peer information
70
- braid_blob.meta_folder = './custom_meta_folder'
71
-
72
- // Set custom peer ID (default: auto-generated and persisted)
73
- braid_blob.peer = 'my-server-id'
52
+ // Set custom storage location (default: './braid-blobs')
53
+ braid_blob.db_folder = './my-blobs'
74
54
  ```
75
55
 
76
56
  ### `braid_blob.serve(req, res, options)`
@@ -88,6 +68,21 @@ Handles HTTP requests for blob storage and synchronization.
88
68
  - `PUT` - Store/update a blob
89
69
  - `DELETE` - Remove a blob
90
70
 
71
+ ### `braid_blob.sync(key, url, options)`
72
+
73
+ Bidirectionally synchronizes a blob between local storage and a remote URL.
74
+
75
+ **Parameters:**
76
+ - `key` - Local storage key (string)
77
+ - `url` - Remote URL (URL object)
78
+ - `options` - Optional configuration object
79
+ - `signal` - AbortSignal for cancellation (use to stop sync)
80
+ - `content_type` / `accept` - Content type for requests
81
+ - `on_pre_connect` - Async callback before connection attempt
82
+ - `on_disconnect` - Callback when connection drops
83
+ - `on_unauthorized` - Callback on 401/403 responses
84
+ - `on_res` - Callback receiving the response object
85
+
91
86
  ### `braid_blob.get(key, options)`
92
87
 
93
88
  Retrieves a blob from local storage or a remote URL.
@@ -113,30 +108,63 @@ Stores a blob to local storage or a remote URL.
113
108
  - `body` - Buffer or data to store
114
109
  - `options` - Optional configuration object
115
110
  - `version` - Version identifier
116
- - `content_type` / `accept` - Content type of the blob
111
+ - `content_type` - Content type of the blob
117
112
  - `signal` - AbortSignal for cancellation
118
113
 
119
- ### `braid_blob.sync(a, b, options)`
114
+ ### `braid_blob.delete(key, options)`
120
115
 
121
- Bidirectionally synchronizes blobs between two endpoints (local keys or URLs).
116
+ Deletes a blob from local storage or a remote URL.
122
117
 
123
118
  **Parameters:**
124
- - `a` - First endpoint (local key or URL)
125
- - `b` - Second endpoint (local key or URL)
119
+ - `key` - Local storage key (string) or remote URL (URL object)
126
120
  - `options` - Optional configuration object
127
- - `signal` - AbortSignal for cancellation (use to stop sync)
128
- - `content_type` / `accept` - Content type for requests
129
- - `on_pre_connect` - Async callback before connection attempt
130
- - `on_disconnect` - Callback when connection drops
131
- - `on_unauthorized` - Callback on 401/403 responses
121
+ - `signal` - AbortSignal for cancellation
122
+
123
+ ## Browser Client
124
+
125
+ A simple browser client is included for subscribing to blob updates.
126
+
127
+ ```html
128
+ <script src="https://unpkg.com/braid-http@~1.3/braid-http-client.js"></script>
129
+ <script src="/client.js"></script>
130
+ <script>
131
+ braid_blob_client('http://localhost:8888/blob.png', {
132
+ on_update: (blob, content_type, version) => {
133
+ // Called whenever the blob is updated
134
+ var url = URL.createObjectURL(new Blob([blob], { type: content_type }))
135
+ document.getElementById('image').src = url
136
+ },
137
+ on_delete: () => console.log('Blob was deleted'),
138
+ on_error: (e) => console.error('Error:', e)
139
+ })
140
+ </script>
141
+ ```
142
+
143
+ ### `braid_blob_client(url, options)`
144
+
145
+ Subscribes to a blob endpoint and receives updates.
146
+
147
+ **Parameters:**
148
+ - `url` - The blob endpoint URL
149
+ - `options` - Configuration object
150
+ - `on_update(blob, content_type, version)` - Callback for updates
151
+ - `on_delete` - Callback when blob is deleted
152
+ - `on_error` - Callback for errors
132
153
  - `on_res` - Callback receiving the response object
133
154
 
155
+ **Returns:** `{ stop }` - Call `stop()` to unsubscribe.
156
+
134
157
  ## Testing
135
158
 
136
- ### to run unit tests:
137
- first run the test server:
159
+ ```bash
160
+ npm install
161
+ node test/test.js
162
+ ```
163
+
164
+ Or run tests in the browser:
138
165
 
139
- npm install
140
- node test/server.js
166
+ ```bash
167
+ node test/test.js -b
168
+ ```
141
169
 
142
- then open http://localhost:8889/test.html, and the boxes should turn green as the tests pass.
170
+ Then open http://localhost:8889/test.html
@@ -0,0 +1,146 @@
1
+ <body style="margin: 0; padding: 20px; font-family: sans-serif;">
2
+ <div id="drop_overlay" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 100, 200, 0.8); z-index: 1000; justify-content: center; align-items: center;">
3
+ <div style="color: white; font-size: 24px; text-align: center;">
4
+ <div style="font-size: 48px; margin-bottom: 20px;">Drop file here</div>
5
+ <div>to upload to <span id="drop_url"></span></div>
6
+ </div>
7
+ </div>
8
+ <h2>Braid Blob Client Demo</h2>
9
+ <p>
10
+ <label>URL: <input type="text" id="url_input" style="width: 400px;" /></label>
11
+ </p>
12
+ <p>Version: <code id="version_display">-</code></p>
13
+ <p>Content-Type: <code id="content_type_display">-</code></p>
14
+ <div id="image_container">
15
+ <img id="blob_image" style="max-width: 100%; border: 1px solid #ccc;" />
16
+ </div>
17
+ <p id="status">Enter a URL and click Connect</p>
18
+ </body>
19
+ <script src="https://unpkg.com/braid-http@~1.3/braid-http-client.js"></script>
20
+ <script src="/client.js"></script>
21
+ <script>
22
+ var current_object_url = null
23
+ var current_client = null
24
+ var current_blob_url = null
25
+
26
+ // Initialize URL input from query parameter, or default to /test_blob
27
+ var params = new URLSearchParams(location.search)
28
+ var initial_url = params.get('url') || `${location.origin}/blob.png`
29
+ document.getElementById('url_input').value = initial_url
30
+
31
+ function connect(blob_url) {
32
+ current_blob_url = blob_url
33
+ // Stop existing subscription
34
+ if (current_client) {
35
+ current_client.stop()
36
+ current_client = null
37
+ }
38
+
39
+ // Clear previous image
40
+ if (current_object_url) {
41
+ URL.revokeObjectURL(current_object_url)
42
+ current_object_url = null
43
+ }
44
+ document.getElementById('blob_image').src = ''
45
+ document.getElementById('version_display').textContent = '-'
46
+ document.getElementById('content_type_display').textContent = '-'
47
+ document.getElementById('status').textContent = 'Connecting...'
48
+
49
+ current_client = braid_blob_client(blob_url, {
50
+ on_update: (blob, content_type, version) => {
51
+ document.getElementById('status').textContent = ''
52
+ document.getElementById('version_display').textContent =
53
+ version ? JSON.stringify(version) : '-'
54
+ document.getElementById('content_type_display').textContent =
55
+ content_type || '-'
56
+
57
+ // Revoke old object URL to free memory
58
+ if (current_object_url) {
59
+ URL.revokeObjectURL(current_object_url)
60
+ }
61
+
62
+ // Create a blob URL from the binary data and display it
63
+ var image_blob = new Blob([blob], { type: content_type || 'image/png' })
64
+ current_object_url = URL.createObjectURL(image_blob)
65
+ document.getElementById('blob_image').src = current_object_url
66
+ },
67
+ on_error: (e) => {
68
+ document.getElementById('status').textContent = 'Connecting...'
69
+ },
70
+ on_delete: () => {
71
+ document.getElementById('status').textContent = 'Blob deleted'
72
+ document.getElementById('blob_image').src = ''
73
+ if (current_object_url) {
74
+ URL.revokeObjectURL(current_object_url)
75
+ current_object_url = null
76
+ }
77
+ }
78
+ })
79
+ }
80
+
81
+ document.getElementById('url_input').onkeydown = (e) => {
82
+ if (e.key === 'Enter') {
83
+ var url = document.getElementById('url_input').value.trim()
84
+ if (url) connect(url)
85
+ }
86
+ }
87
+
88
+ // Auto-connect on load
89
+ connect(initial_url)
90
+
91
+ // Drag and drop handling
92
+ var drop_overlay = document.getElementById('drop_overlay')
93
+ var drag_counter = 0
94
+
95
+ document.addEventListener('dragenter', (e) => {
96
+ e.preventDefault()
97
+ drag_counter++
98
+ if (drag_counter === 1) {
99
+ document.getElementById('drop_url').textContent = current_blob_url || 'the current URL'
100
+ drop_overlay.style.display = 'flex'
101
+ }
102
+ })
103
+
104
+ document.addEventListener('dragleave', (e) => {
105
+ e.preventDefault()
106
+ drag_counter--
107
+ if (drag_counter === 0) {
108
+ drop_overlay.style.display = 'none'
109
+ }
110
+ })
111
+
112
+ document.addEventListener('dragover', (e) => {
113
+ e.preventDefault()
114
+ })
115
+
116
+ document.addEventListener('drop', async (e) => {
117
+ e.preventDefault()
118
+ drag_counter = 0
119
+ drop_overlay.style.display = 'none'
120
+
121
+ if (!current_blob_url) {
122
+ alert('Please connect to a URL first')
123
+ return
124
+ }
125
+
126
+ var file = e.dataTransfer.files[0]
127
+ if (!file) return
128
+
129
+ document.getElementById('status').textContent = 'Uploading...'
130
+
131
+ try {
132
+ var response = await fetch(current_blob_url, {
133
+ method: 'PUT',
134
+ headers: { 'Content-Type': file.type || 'application/octet-stream' },
135
+ body: file
136
+ })
137
+ if (!response.ok) {
138
+ document.getElementById('status').textContent = 'Upload failed: ' + response.status
139
+ }
140
+ // The subscription will automatically show the new image
141
+ } catch (err) {
142
+ document.getElementById('status').textContent = 'Upload error: ' + err.message
143
+ console.error('Upload error:', err)
144
+ }
145
+ })
146
+ </script>
package/client.js ADDED
@@ -0,0 +1,79 @@
1
+ // Braid-Blob Client
2
+ // requires braid-http@~1.3/braid-http-client.js
3
+ //
4
+ // Usage:
5
+ // braid_blob_client(url, {
6
+ // on_update: (blob, content_type, version) => {
7
+ // // Called whenever there's a new version of the blob
8
+ // console.log('New blob:', blob, content_type, version)
9
+ // }
10
+ // })
11
+ //
12
+ // Returns: { stop } - call stop() to unsubscribe
13
+ //
14
+ function braid_blob_client(url, options = {}) {
15
+ var ac = new AbortController()
16
+ var peer = Math.random().toString(36).substr(2)
17
+ var current_version = null
18
+
19
+ braid_fetch(url, {
20
+ headers: { "Merge-Type": "aww" },
21
+ subscribe: true,
22
+ retry: () => true,
23
+ peer,
24
+ signal: ac.signal
25
+ }).then(res => {
26
+ if (options.on_res) options.on_res(res)
27
+
28
+ res.subscribe(async update => {
29
+ if (update.status === 404) {
30
+ current_version = null
31
+ if (options.on_delete) options.on_delete()
32
+ return
33
+ }
34
+
35
+ var content_type = update.extra_headers?.['content-type']
36
+ var version = update.version?.[0]
37
+
38
+ // Only update if version is newer
39
+ if (compare_events(version, current_version) > 0) {
40
+ current_version = version
41
+ if (options.on_update) options.on_update(update.body, content_type, update.version)
42
+ }
43
+ }, options.on_error || (e => console.error('braid_blob_client error:', e)))
44
+ }).catch(options.on_error || (e => console.error('braid_blob_client error:', e)))
45
+
46
+ return {
47
+ stop: () => ac.abort()
48
+ }
49
+
50
+ function compare_events(a, b) {
51
+ if (!a) a = ''
52
+ if (!b) b = ''
53
+
54
+ var c = compare_seqs(get_event_seq(a), get_event_seq(b))
55
+ if (c) return c
56
+
57
+ if (a < b) return -1
58
+ if (a > b) return 1
59
+ return 0
60
+ }
61
+
62
+ function get_event_seq(e) {
63
+ if (!e) return ''
64
+
65
+ for (let i = e.length - 1; i >= 0; i--)
66
+ if (e[i] === '-') return e.slice(i + 1)
67
+ return e
68
+ }
69
+
70
+ function compare_seqs(a, b) {
71
+ if (!a) a = ''
72
+ if (!b) b = ''
73
+
74
+ if (a.length !== b.length) return a.length - b.length
75
+ if (a < b) return -1
76
+ if (a > b) return 1
77
+ return 0
78
+ }
79
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-blob",
3
- "version": "0.0.53",
3
+ "version": "0.0.55",
4
4
  "description": "Library for collaborative blobs over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braid-blob",
package/server-demo.js CHANGED
@@ -3,22 +3,40 @@ var port = process.argv[2] || 8888
3
3
  var braid_blob = require(`${__dirname}/index.js`)
4
4
 
5
5
  // TODO: set a custom storage base
6
- // (the default is ./braid-blob-files)
6
+ // (the default is ./braid-blobs)
7
7
  //
8
8
  // braid_blob.db_folder = './custom_files_folder'
9
- // braid_blob.meta_folder = './custom_meta_folder'
10
9
 
11
10
  braid_blob.init()
12
11
 
13
12
  var server = require("http").createServer(async (req, res) => {
14
13
  console.log(`${req.method} ${req.url}`)
15
14
 
15
+ var url = req.url.split('?')[0]
16
+
17
+ if (url === '/client.js') {
18
+ res.writeHead(200, {
19
+ "Content-Type": "text/javascript",
20
+ "Cache-Control": "no-cache"
21
+ })
22
+ require("fs").createReadStream(`${__dirname}/client.js`).pipe(res)
23
+ return
24
+ }
25
+
26
+ if (url === '/') {
27
+ res.writeHead(200, {
28
+ "Content-Type": "text/html",
29
+ "Cache-Control": "no-cache"
30
+ })
31
+ require("fs").createReadStream(`${__dirname}/client-demo.html`).pipe(res)
32
+ return
33
+ }
34
+
16
35
  braid_blob.serve(req, res)
17
36
  })
18
37
 
19
38
  server.listen(port, () => {
20
- console.log(`server started on port ${port}`)
21
- console.log(`files stored in: ${braid_blob.db_folder}`)
39
+ console.log(`server started on http://localhost:${port}`)
22
40
  })
23
41
 
24
42
  // curl -X PUT -H "Content-Type: image/png" --data-binary @blob.png http://localhost:8888/blob.png