braid-blob 0.0.59 → 0.0.61

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
@@ -46,13 +46,24 @@ node server-demo.js
46
46
 
47
47
  Now open up http://localhost:8888 in your browser, to see the client. Open two windows. You can drag and drop images between them, and they will always stay synchronized.
48
48
 
49
- <video src="https://github.com/user-attachments/assets/24136939-613f-4d97-9803-f52828f00536" controls width="600"></video>
49
+ <video src="https://github.com/user-attachments/assets/66ba0004-138c-4faa-a1d5-cb2cd06d3525" controls width="600"></video>
50
50
 
51
51
  ## Network API
52
52
 
53
53
  Braid-blob speaks [Braid-HTTP](https://github.com/braid-org/braid-spec), an extension to HTTP for synchronization.
54
54
 
55
- ### GET - Retrieve a blob
55
+ ### Special Braid-HTTP Headers
56
+
57
+ | Header | Description |
58
+ |--------|-------------|
59
+ | `Version` | Unique identifier for this version of the blob (e.g., `"alice-42"`) |
60
+ | `Parents` | The previous version |
61
+ | `Merge-Type` | How conflicts resolve consistently (*e.g.* `aww` for [arbitrary-writer-wins](https://braid.org/protocol/merge-types/aww)) |
62
+ | `Subscribe` | In GET, subscribes client to all future changes |
63
+ | `Accept-Subscribe` | Server indicates it supports subscriptions |
64
+ | `Current-Version` | The latest version that the server is aware of |
65
+
66
+ ### GET retrieves a blob
56
67
 
57
68
  ```http
58
69
  GET /blob.png HTTP/1.1
@@ -62,12 +73,18 @@ Response:
62
73
 
63
74
  ```http
64
75
  HTTP/1.1 200 OK
76
+ Version: "alice-1"
65
77
  Content-Type: image/png
78
+ Merge-Type: aww
79
+ Accept-Subscribe: true
80
+ Content-Length: 12345
66
81
 
67
82
  <binary data>
68
83
  ```
69
84
 
70
- ### GET with Subscribe - Real-time updates
85
+ Returns `404 Not Found` if the blob doesn't exist.
86
+
87
+ ### GET with Subscribe syncs client with realtime updates
71
88
 
72
89
  Add `Subscribe: true` to receive updates whenever the blob changes:
73
90
 
@@ -83,6 +100,7 @@ HTTP/1.1 209 Subscription
83
100
  Subscribe: true
84
101
  Current-Version: "alice-1"
85
102
 
103
+ HTTP 200 OK
86
104
  Version: "alice-1"
87
105
  Content-Type: image/png
88
106
  Merge-Type: aww
@@ -90,6 +108,7 @@ Content-Length: 12345
90
108
 
91
109
  <binary data>
92
110
 
111
+ HTTP 200 OK
93
112
  Version: "bob-2"
94
113
  Content-Type: image/png
95
114
  Merge-Type: aww
@@ -99,23 +118,45 @@ Content-Length: 23456
99
118
  ...
100
119
  ```
101
120
 
102
- ### PUT - Store a blob
121
+ If the blob doesn't exist yet, `Current-Version` will be blank and no initial update is sent. If the blob is deleted, a `404` update is streamed.
122
+
123
+ ### PUT stores a blob
103
124
 
104
125
  ```http
105
126
  PUT /blob.png HTTP/1.1
106
127
  Version: "carol-3"
107
128
  Content-Type: image/png
108
129
  Merge-Type: aww
130
+ Content-Length: 34567
109
131
 
110
132
  <binary data>
111
133
  ```
112
134
 
113
- ### DELETE - Remove a blob
135
+ Response:
136
+
137
+ ```http
138
+ HTTP/1.1 200 OK
139
+ Version: "carol-3"
140
+ ```
141
+
142
+ If the sent version is older or eclipsed by the server's current version, the returned `Version` will be the server's version (not the one you sent).
143
+
144
+ The `braid_blob.serve()` method (below) will accept every PUT sent to it, but you can implement access control for any request before passing it to `serve()`, and return e.g. `401 Unauthorized` if you do no want to allow the PUT.
145
+
146
+ ### DELETE removes a blob
114
147
 
115
148
  ```http
116
149
  DELETE /blob.png HTTP/1.1
117
150
  ```
118
151
 
152
+ Response:
153
+
154
+ ```http
155
+ HTTP/1.1 200 OK
156
+ ```
157
+
158
+ Returns `200 OK` even if the blob didn't exist.
159
+
119
160
  ### Understanding versions
120
161
 
121
162
  Versions look like `"alice-42"` where:
@@ -0,0 +1,35 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Live Image Demo</title>
5
+ <style>
6
+ body {
7
+ font-family: system-ui, sans-serif;
8
+ max-width: 800px;
9
+ margin: 40px auto;
10
+ padding: 20px;
11
+ }
12
+ img[live] {
13
+ max-width: 100%;
14
+ border: 2px solid #ccc;
15
+ border-radius: 8px;
16
+ }
17
+ .controls {
18
+ margin: 20px 0;
19
+ }
20
+ input[type="file"] {
21
+ margin-right: 10px;
22
+ }
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <h1>Braid-Blob Live Image Demo</h1>
27
+ <p>This image updates in real-time across all connected clients.</p>
28
+
29
+ <img live src="/blob.png" alt="Live updating image">
30
+
31
+ <script src="https://unpkg.com/braid-http@~1.3/braid-http-client.js"></script>
32
+ <script src="client.js"></script>
33
+ <script src="img-live.js"></script>
34
+ </body>
35
+ </html>
package/img-live.js ADDED
@@ -0,0 +1,108 @@
1
+ // Braid-Blob Live Images
2
+ // requires client.js
3
+
4
+ var live_images = new Map() // url -> { blob, images: Set<img> }
5
+
6
+ function sync(img) {
7
+ var url = img.src
8
+ if (!url) return
9
+
10
+ var entry = live_images.get(url)
11
+ if (entry) {
12
+ entry.images.add(img)
13
+ // Apply current blob URL if we have one
14
+ if (entry.objectUrl)
15
+ img.src = entry.objectUrl
16
+ return
17
+ }
18
+
19
+ // Create new subscription for this URL
20
+ entry = { images: new Set([img]), objectUrl: null, blob: null }
21
+ live_images.set(url, entry)
22
+
23
+ entry.blob = braid_blob_client(url, {
24
+ on_update: (body, content_type) => {
25
+ // Revoke old object URL if exists
26
+ if (entry.objectUrl)
27
+ URL.revokeObjectURL(entry.objectUrl)
28
+
29
+ // Create new blob and object URL
30
+ var blob = new Blob([body], { type: content_type || 'image/png' })
31
+ entry.objectUrl = URL.createObjectURL(blob)
32
+
33
+ // Update all images subscribed to this URL
34
+ entry.images.forEach(img => {
35
+ img.src = entry.objectUrl
36
+ })
37
+ },
38
+ on_delete: () => {
39
+ if (entry.objectUrl) {
40
+ URL.revokeObjectURL(entry.objectUrl)
41
+ entry.objectUrl = null
42
+ }
43
+ },
44
+ on_error: (error) => {
45
+ console.error('Live image error for', url, error)
46
+ }
47
+ })
48
+ }
49
+
50
+ function unsync(img) {
51
+ // Find which entry this image belongs to
52
+ for (var [url, entry] of live_images) {
53
+ if (entry.images.has(img)) {
54
+ entry.images.delete(img)
55
+
56
+ // If no more images using this URL, clean up
57
+ if (entry.images.size === 0) {
58
+ if (entry.objectUrl)
59
+ URL.revokeObjectURL(entry.objectUrl)
60
+ // Note: braid_blob_client doesn't expose unsubscribe,
61
+ // would need to pass AbortSignal in options to cancel
62
+ live_images.delete(url)
63
+ }
64
+ break
65
+ }
66
+ }
67
+ }
68
+
69
+ var observer = new MutationObserver(function(mutations) {
70
+ mutations.forEach(function(mutation) {
71
+ mutation.addedNodes.forEach(function(node) {
72
+ if (node.nodeType === 1) {
73
+ if (node.tagName === 'IMG' && node.hasAttribute('live'))
74
+ sync(node)
75
+ node.querySelectorAll('img[live]').forEach(sync)
76
+ }
77
+ })
78
+ mutation.removedNodes.forEach(function(node) {
79
+ if (node.nodeType === 1) {
80
+ if (node.tagName === 'IMG' && node.hasAttribute('live'))
81
+ unsync(node)
82
+ node.querySelectorAll('img[live]').forEach(unsync)
83
+ }
84
+ })
85
+ if (mutation.type === 'attributes' && mutation.attributeName === 'live' && mutation.target.tagName === 'IMG') {
86
+ if (mutation.target.hasAttribute('live'))
87
+ sync(mutation.target)
88
+ else
89
+ unsync(mutation.target)
90
+ }
91
+ })
92
+ })
93
+
94
+ if (document.readyState === 'loading')
95
+ document.addEventListener('DOMContentLoaded', init)
96
+ else
97
+ init()
98
+
99
+ function init() {
100
+ observer.observe(document.body, {
101
+ childList: true,
102
+ subtree: true,
103
+ attributes: true,
104
+ attributeFilter: ['live']
105
+ })
106
+
107
+ document.querySelectorAll('img[live]').forEach(sync)
108
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-blob",
3
- "version": "0.0.59",
3
+ "version": "0.0.61",
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
@@ -23,7 +23,16 @@ var server = require("http").createServer(async (req, res) => {
23
23
  return
24
24
  }
25
25
 
26
- if (url === '/') {
26
+ if (url === '/img-live.js') {
27
+ res.writeHead(200, {
28
+ "Content-Type": "text/javascript",
29
+ "Cache-Control": "no-cache"
30
+ })
31
+ require("fs").createReadStream(`${__dirname}/img-live.js`).pipe(res)
32
+ return
33
+ }
34
+
35
+ if (url === '/' || url === '/client-demo.html') {
27
36
  res.writeHead(200, {
28
37
  "Content-Type": "text/html",
29
38
  "Cache-Control": "no-cache"
@@ -32,6 +41,15 @@ var server = require("http").createServer(async (req, res) => {
32
41
  return
33
42
  }
34
43
 
44
+ if (url === '/img-live-demo.html') {
45
+ res.writeHead(200, {
46
+ "Content-Type": "text/html",
47
+ "Cache-Control": "no-cache"
48
+ })
49
+ require("fs").createReadStream(`${__dirname}/img-live-demo.html`).pipe(res)
50
+ return
51
+ }
52
+
35
53
  braid_blob.serve(req, res)
36
54
  })
37
55