braid-blob 0.0.60 → 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,24 +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/0efc9fdc-71c8-4437-ac54-5b6dca30ac66" 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
- ### Braid-Specific Headers
55
+ ### Special Braid-HTTP Headers
56
56
 
57
57
  | Header | Description |
58
58
  |--------|-------------|
59
59
  | `Version` | Unique identifier for this version of the blob (e.g., `"alice-42"`) |
60
- | `Parents` | Used to request updates newer than a known version |
61
- | `Merge-Type` | Conflict resolution strategy; `aww` means "arbitrary-writer-wins" |
62
- | `Subscribe` | Request a persistent connection that streams updates |
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
63
  | `Accept-Subscribe` | Server indicates it supports subscriptions |
64
- | `Current-Version` | The version the server currently has |
64
+ | `Current-Version` | The latest version that the server is aware of |
65
65
 
66
- ### GET - Retrieve a blob
66
+ ### GET retrieves a blob
67
67
 
68
68
  ```http
69
69
  GET /blob.png HTTP/1.1
@@ -84,7 +84,7 @@ Content-Length: 12345
84
84
 
85
85
  Returns `404 Not Found` if the blob doesn't exist.
86
86
 
87
- ### GET with Subscribe - Real-time updates
87
+ ### GET with Subscribe syncs client with realtime updates
88
88
 
89
89
  Add `Subscribe: true` to receive updates whenever the blob changes:
90
90
 
@@ -120,7 +120,7 @@ Content-Length: 23456
120
120
 
121
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
122
 
123
- ### PUT - Store a blob
123
+ ### PUT stores a blob
124
124
 
125
125
  ```http
126
126
  PUT /blob.png HTTP/1.1
@@ -139,9 +139,11 @@ HTTP/1.1 200 OK
139
139
  Version: "carol-3"
140
140
  ```
141
141
 
142
- The PUT always succeeds, but if the sent version is eclipsed by the server's current version, the returned `Version` will be the server's version (not the one you sent).
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
143
 
144
- ### DELETE - Remove a blob
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
145
147
 
146
148
  ```http
147
149
  DELETE /blob.png HTTP/1.1
@@ -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.60",
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