braid-blob 0.0.67 → 0.0.69

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.
Files changed (4) hide show
  1. package/README.md +98 -74
  2. package/client.js +50 -23
  3. package/index.js +59 -22
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -56,7 +56,8 @@ Braid-blob speaks [Braid-HTTP](https://github.com/braid-org/braid-spec), an exte
56
56
 
57
57
  | Header | Description |
58
58
  |--------|-------------|
59
- | `Version` | Unique identifier for this version of the blob (e.g., `"alice-42"`) |
59
+ | `Version` | Unique identifier for this version of the blob (e.g., `"1768467700000"`) |
60
+ | `Version-Type` | How to interpret the structure of version strings (e.g., `relative-wallclock`); see [Version-Type spec](https://github.com/braid-org/braid-spec/blob/master/draft-toomim-httpbis-versions-03.txt) |
60
61
  | `Parents` | The previous version |
61
62
  | `Merge-Type` | How conflicts resolve consistently (*e.g.* `aww` for [arbitrary-writer-wins](https://braid.org/protocol/merge-types/aww)) |
62
63
  | `Subscribe` | In GET, subscribes client to all future changes |
@@ -73,7 +74,8 @@ Response:
73
74
 
74
75
  ```http
75
76
  HTTP/1.1 200 OK
76
- Version: "alice-1"
77
+ Version: "1768467700000"
78
+ Version-Type: relative-wallclock
77
79
  Content-Type: image/png
78
80
  Merge-Type: aww
79
81
  Accept-Subscribe: true
@@ -98,10 +100,12 @@ Response (keeps connection open, streams updates):
98
100
  ```http
99
101
  HTTP/1.1 209 Multiresponse
100
102
  Subscribe: true
101
- Current-Version: "alice-1"
103
+ Current-Version: "1768467700000"
104
+ Version-Type: relative-wallclock
102
105
 
103
106
  HTTP 200 OK
104
- Version: "alice-1"
107
+ Version: "1768467700000"
108
+ Version-Type: relative-wallclock
105
109
  Content-Type: image/png
106
110
  Merge-Type: aww
107
111
  Content-Length: 12345
@@ -109,7 +113,8 @@ Content-Length: 12345
109
113
  <binary data>
110
114
 
111
115
  HTTP 200 OK
112
- Version: "bob-2"
116
+ Version: "1768467701000"
117
+ Version-Type: relative-wallclock
113
118
  Content-Type: image/png
114
119
  Merge-Type: aww
115
120
  Content-Length: 23456
@@ -124,7 +129,8 @@ If the blob doesn't exist yet, `Current-Version` will be blank and no initial up
124
129
 
125
130
  ```http
126
131
  PUT /blob.png HTTP/1.1
127
- Version: "carol-3"
132
+ Version: "1768467702000"
133
+ Version-Type: relative-wallclock
128
134
  Content-Type: image/png
129
135
  Merge-Type: aww
130
136
  Content-Length: 34567
@@ -136,7 +142,8 @@ Response:
136
142
 
137
143
  ```http
138
144
  HTTP/1.1 200 OK
139
- Current-Version: "carol-3"
145
+ Current-Version: "1768467702000"
146
+ Version-Type: relative-wallclock
140
147
  ```
141
148
 
142
149
  If the sent version is older or eclipsed by the server's current version, the returned `Current-Version` will be the server's version (not the one you sent).
@@ -159,11 +166,9 @@ Returns `200 OK` even if the blob didn't exist.
159
166
 
160
167
  ### Understanding versions
161
168
 
162
- Versions look like `"alice-42"` where:
163
- - `alice` is a peer ID (identifies who made the change)
164
- - `42` is a sequence number (generally milliseconds past the epoch, or one plus the current number if it is past the current time)
169
+ Versions are timestamps representing milliseconds past the epoch (e.g., `"1768467700000"`). If the current time is less than the latest known version, a small random number is added to the current version to provide entropy in case multiple peers are writing simultaneously.
165
170
 
166
- Conflicts resolve using ["arbitrary-writer-wins" (AWW)](https://braid.org/protocol/merge-types/aww): the version with the highest sequence number wins. If sequences match, the peer ID string is compared lexicographically.
171
+ Conflicts resolve using ["arbitrary-writer-wins" (AWW)](https://braid.org/protocol/merge-types/aww): the version with the highest timestamp wins.
167
172
 
168
173
 
169
174
 
@@ -178,97 +183,95 @@ var braid_blob = require('braid-blob')
178
183
  braid_blob.db_folder = './braid-blobs' // Default: ./braid-blobs
179
184
  ```
180
185
 
181
- ### Serve blobs to HTTP Requests (GET, PUT, and DELETE)
186
+ ### Examples
182
187
 
183
- Your app becomes a blob server with:
188
+ #### Get a blob
184
189
 
185
190
  ```javascript
186
- braid_blob.serve(req, res, params)
191
+ // Get a local blob:
192
+ var {body, version, content_type} = await braid_blob.get('foo')
193
+
194
+ // Get a remote blob:
195
+ var {body, version, content_type} = await braid_blob.get(new URL('https://foo.bar/baz'))
196
+
197
+ // Get a specific version of a remote blob:
198
+ var {body} = await braid_blob.get(
199
+ new URL('https://foo.bar/baz'),
200
+ {version: ['5zb2sjdstmk-1768093765048']}
201
+ )
202
+
203
+ // To subscribe to a remote blob, without storing updates locally:
204
+ await braid_blob.get(new URL('https://foo.bar/baz'), {
205
+ subscribe: (update) => {
206
+ console.log('Got update:', update.version, update.content_type)
207
+ // update.body contains the new blob data
208
+ }
209
+ })
210
+
211
+ // To mirror a remote blob to local storage (bidirectional sync):
212
+ var ac = new AbortController()
213
+ braid_blob.sync('local-key', new URL('https://foo.bar/baz'), {signal: ac.signal})
214
+ // Later, stop syncing:
215
+ ac.abort()
187
216
  ```
188
217
 
189
- This will synchronize the client issuing the given request and response with its blob on disk.
218
+ Note: `.get()` with `subscribe` receives updates but does not store them locally. `.sync()` performs two subscriptions (local↔remote) plus auto-forwards updates in both directions.
190
219
 
191
- Parameters:
192
- - `req` - HTTP request object
193
- - `res` - HTTP response object
194
- - `params` - Optional configuration object
195
- - `key` - The blob on disk to sync with (default: `req.url`)
196
-
197
- ### Sync a remotely served blob to disk
198
-
199
- Your app becomes a blob client with:
220
+ #### Put a blob
200
221
 
201
222
  ```javascript
202
- braid_blob.sync(key, url, params)
223
+ // Write to a local blob:
224
+ await braid_blob.put('foo', Buffer.from('hello'), {content_type: 'text/plain'})
225
+
226
+ // Write to a remote blob:
227
+ await braid_blob.put(
228
+ new URL('https://foo.bar/baz'),
229
+ Buffer.from('hello'),
230
+ {content_type: 'text/plain'}
231
+ )
203
232
  ```
204
233
 
205
- Synchronizes a remote URL to its blob on disk.
206
-
207
- Parameters:
208
- - `key` - The blob on disk (string)
209
- - `url` - Remote URL (URL object)
210
- - `params` - Optional configuration object
211
- - `signal` - AbortSignal for cancellation (use to stop sync)
212
- - `content_type` - Content type for requests
213
-
214
- ### Read, Write or Delete a blob
215
-
216
- #### Read a local or remote blob
234
+ #### Delete a blob
217
235
 
218
236
  ```javascript
219
- braid_blob.get(key, params)
237
+ // Delete a local blob:
238
+ await braid_blob.delete('foo')
239
+
240
+ // Delete a remote blob:
241
+ await braid_blob.delete(new URL('https://foo.bar/baz'))
220
242
  ```
221
243
 
222
- Retrieves a blob from local storage or a remote URL.
244
+ ### API Reference
223
245
 
224
- Examples:
225
- ```javascript
226
- // Get the current contents of a local blob:
227
- braid_blob.get('foo').body
246
+ #### braid_blob.get(key, params)
228
247
 
229
- // Get the contents of a remote blob:
230
- braid_blob.get(new URL('https://foo.bar/baz')).body
231
-
232
- // Get an old version of a remote blob:
233
- braid_blob.get(
234
- new URL('https://foo.bar/baz'),
235
- {version: ["5zb2sjdstmk-1768093765048"]}
236
- ).body
237
- ```
248
+ Retrieves a blob from local storage or a remote URL.
238
249
 
239
250
  Parameters:
240
251
  - `key` - The local blob (if string) or remote URL (if [URL object](https://nodejs.org/api/url.html#class-url)) to read from
241
252
  - `params` - Optional configuration object
242
- - `version` - Version ID to check existence (use with `head: true`)
243
- - `parent` - Version ID; when subscribing, only receive updates newer than this
244
- - `subscribe` - Callback function for real-time updates
245
- - `head` - If true, returns only metadata (version, content_type) without body
246
- - `content_type` - Content type for the request
247
- - `signal` - AbortSignal for cancellation
253
+ - `version` - Retrieve a specific version instead of the latest (e.g., `['abc-123']`)
254
+ - `parents` - When subscribing, only receive updates newer than this version (e.g., `['abc-123']`)
255
+ - `subscribe` - Callback `(update) => {}` called with each update; `update` has `{body, version, content_type}`
256
+ - `head` - If `true`, returns only metadata (`{version, content_type}`) without the body—useful for checking if a blob exists or getting its current version
257
+ - `content_type` - Expected content type (sent as Accept header for remote URLs)
258
+ - `signal` - AbortSignal to cancel the request or stop a subscription
248
259
 
249
- Returns: `{version, body, content_type}` object, or `null` if not found.
260
+ Returns: `{version, body, content_type}` object, or `null` if the blob doesn't exist. When subscribing to a remote URL, returns the fetch response object; updates are delivered via the callback.
250
261
 
251
- #### Write a local or remote blob
262
+ #### braid_blob.put(key, body, params)
252
263
 
253
- ```javascript
254
- braid_blob.put(key, body, params)
255
- ```
256
-
257
- Writes a blob to local storage or a remote URL. Any other peers synchronizing with this blob (via `.serve()`, `.sync()`, or `.get(.., {subscribe: ..}`) will be updated.
264
+ Writes a blob to local storage or a remote URL. Any other peers synchronizing with this blob (via `.serve()`, `.sync()`, or `.get(.., {subscribe: ..})`) will be updated.
258
265
 
259
266
  Parameters:
260
267
  - `key` - The local blob (if string) or remote URL (if [URL object](https://nodejs.org/api/url.html#class-url)) to write to
261
- - `body` - Buffer or data to store
268
+ - `body` - The data to store (Buffer, ArrayBuffer, or Uint8Array)
262
269
  - `params` - Optional configuration object
263
- - `version` - Version identifier
264
- - `content_type` - Content type of the blob
265
- - `signal` - AbortSignal for cancellation
270
+ - `version` - Specify a version ID for this write (e.g., `['my-version-1']`); if omitted, one is generated automatically
271
+ - `content_type` - MIME type of the blob (e.g., `'image/png'`, `'application/json'`)
272
+ - `signal` - AbortSignal to cancel the request
266
273
 
267
- #### Delete a local or remote blob
268
-
269
- ```javascript
270
- braid_blob.delete(key, params)
271
- ```
274
+ #### braid_blob.delete(key, params)
272
275
 
273
276
  Deletes a blob from local storage or a remote URL.
274
277
 
@@ -277,6 +280,27 @@ Parameters:
277
280
  - `params` - Optional configuration object
278
281
  - `signal` - AbortSignal for cancellation
279
282
 
283
+ #### braid_blob.sync(key, url, params)
284
+
285
+ Synchronizes a remote URL bidirectionally with a local blob on disk. This performs two subscriptions (one to the remote, one to the local blob) and auto-forwards updates in both directions.
286
+
287
+ Parameters:
288
+ - `key` - The local blob on disk (string)
289
+ - `url` - Remote URL (URL object)
290
+ - `params` - Optional configuration object
291
+ - `signal` - AbortSignal for cancellation (use to stop sync)
292
+ - `content_type` - Content type for requests
293
+
294
+ #### braid_blob.serve(req, res, params)
295
+
296
+ Serves blob requests over HTTP. Synchronizes the client issuing the given request with its blob on disk.
297
+
298
+ Parameters:
299
+ - `req` - HTTP request object
300
+ - `res` - HTTP response object
301
+ - `params` - Optional configuration object
302
+ - `key` - The blob on disk to sync with (default: the path from `req.url`)
303
+
280
304
  ## Browser Client API
281
305
 
282
306
  A simple browser client is included for subscribing to blob updates.
package/client.js CHANGED
@@ -51,9 +51,7 @@ function braid_blob_client(url, params = {}) {
51
51
 
52
52
  return {
53
53
  update: async (body, content_type) => {
54
- var seq = max_seq('' + Date.now(),
55
- increment_seq(get_event_seq(current_version)))
56
- current_version = `${peer}-${seq}`
54
+ current_version = create_event(current_version)
57
55
 
58
56
  params.on_update?.(body, content_type, current_version)
59
57
 
@@ -80,41 +78,70 @@ function braid_blob_client(url, params = {}) {
80
78
  return 0
81
79
  }
82
80
 
81
+ function create_event(current_event, max_entropy = 1000) {
82
+ var new_event = '' + Date.now()
83
+
84
+ var current_seq = get_event_seq(current_event)
85
+ if (compare_seqs(new_event, current_seq) > 0) return new_event
86
+
87
+ // Find smallest base-10 integer where compare_seqs(int, current_seq) >= 0
88
+ var base = seq_to_int(current_seq)
89
+ return '' + (base + 1 + Math.floor(Math.random() * max_entropy))
90
+ }
91
+
83
92
  function get_event_seq(e) {
84
93
  if (!e) return ''
85
94
 
86
95
  for (let i = e.length - 1; i >= 0; i--)
87
- if (e[i] === '-') return e.slice(i + 1)
96
+ if (e[i] === '-') return i == 0 ? e : e.slice(i + 1)
88
97
  return e
89
98
  }
90
99
 
91
- function increment_seq(s) {
92
- if (!s) return '1'
93
-
94
- let last = s[s.length - 1]
95
- let rest = s.slice(0, -1)
96
-
97
- if (last >= '0' && last <= '8')
98
- return rest + String.fromCharCode(last.charCodeAt(0) + 1)
99
- else
100
- return increment_seq(rest) + '0'
101
- }
102
-
103
- function max_seq(a, b) {
100
+ function compare_seqs(a, b) {
104
101
  if (!a) a = ''
105
102
  if (!b) b = ''
106
103
 
107
- if (compare_seqs(a, b) > 0) return a
108
- return b
109
- }
104
+ var a_neg = a[0] === '-'
105
+ var b_neg = b[0] === '-'
106
+ if (a_neg !== b_neg) return a_neg ? -1 : 1
110
107
 
111
- function compare_seqs(a, b) {
112
- if (!a) a = ''
113
- if (!b) b = ''
108
+ // Both negative: compare magnitudes (reversed)
109
+ if (a_neg) {
110
+ var swap = a.slice(1); a = b.slice(1); b = swap
111
+ }
114
112
 
115
113
  if (a.length !== b.length) return a.length - b.length
116
114
  if (a < b) return -1
117
115
  if (a > b) return 1
118
116
  return 0
119
117
  }
118
+
119
+ // Smallest base-10 integer n where compare_seqs(String(n), s) >= 0
120
+ function seq_to_int(s) {
121
+ if (!s || s[0] === '-') return 0
122
+
123
+ var len = s.length
124
+ var min_of_len = Math.pow(10, len - 1) // e.g., len=3 -> 100
125
+ var max_of_len = Math.pow(10, len) - 1 // e.g., len=3 -> 999
126
+
127
+ if (s < String(min_of_len)) return min_of_len
128
+ if (s > String(max_of_len)) return max_of_len + 1
129
+
130
+ // s is in the base-10 range for this length
131
+ // scan for first non-digit > '9', increment prefix and pad zeros
132
+ var n = 0
133
+ for (var i = 0; i < len; i++) {
134
+ var c = s.charCodeAt(i)
135
+ if (c >= 48 && c <= 57) {
136
+ n = n * 10 + (c - 48)
137
+ } else if (c > 57) {
138
+ // non-digit > '9': increment prefix, pad rest with zeros
139
+ return (n + 1) * Math.pow(10, len - i)
140
+ } else {
141
+ // non-digit < '0': just pad rest with zeros
142
+ return n * Math.pow(10, len - i)
143
+ }
144
+ }
145
+ return n
146
+ }
120
147
  }
package/index.js CHANGED
@@ -152,6 +152,7 @@ function create_braid_blob() {
152
152
  header_cb: (result) => {
153
153
  res.setHeader((req.subscribe ? "Current-" : "") +
154
154
  "Version", version_to_header(result.version))
155
+ res.setHeader("Version-Type", "relative-wallclock")
155
156
  if (result.content_type)
156
157
  res.setHeader('Content-Type', result.content_type)
157
158
  },
@@ -166,6 +167,7 @@ function create_braid_blob() {
166
167
  delete update.content_type
167
168
  }
168
169
  update['Merge-Type'] = 'aww'
170
+ update['Version-Type'] = 'relative-wallclock'
169
171
  res.sendUpdate(update)
170
172
  } : null
171
173
  })
@@ -204,6 +206,7 @@ function create_braid_blob() {
204
206
  peer: req.peer
205
207
  })
206
208
  res.setHeader("Current-Version", version_to_header(event != null ? [event] : []))
209
+ res.setHeader("Version-Type", "relative-wallclock")
207
210
  res.end('')
208
211
  } else if (req.method === 'DELETE') {
209
212
  await braid_blob.delete(params.key, {
@@ -234,6 +237,9 @@ function create_braid_blob() {
234
237
  if (params.content_type)
235
238
  fetch_params.headers = { ...fetch_params.headers,
236
239
  'Accept': params.content_type }
240
+ if (params.version || params.parents)
241
+ fetch_params.headers = { ...fetch_params.headers,
242
+ 'Version-Type': 'relative-wallclock' }
237
243
 
238
244
  var res = await braid_fetch(key.href, fetch_params)
239
245
 
@@ -345,6 +351,9 @@ function create_braid_blob() {
345
351
  if (params.content_type)
346
352
  fetch_params.headers = { ...fetch_params.headers,
347
353
  'Content-Type': params.content_type }
354
+ if (params.version)
355
+ fetch_params.headers = { ...fetch_params.headers,
356
+ 'Version-Type': 'relative-wallclock' }
348
357
 
349
358
  return await braid_fetch(key.href, fetch_params)
350
359
  }
@@ -358,8 +367,7 @@ function create_braid_blob() {
358
367
 
359
368
  var their_e = params.version ? params.version[0] :
360
369
  // we'll give them a event id in this case
361
- `${braid_blob.peer}-${max_seq('' + Date.now(),
362
- meta.event ? increment_seq(get_event_seq(meta.event)) : '')}`
370
+ create_event(meta.event)
363
371
 
364
372
  if (compare_events(their_e, meta.event) > 0) {
365
373
  meta.event = their_e
@@ -591,37 +599,37 @@ function create_braid_blob() {
591
599
  return 0
592
600
  }
593
601
 
602
+ function create_event(current_event, max_entropy = 1000) {
603
+ var new_event = '' + Date.now()
604
+
605
+ var current_seq = get_event_seq(current_event)
606
+ if (compare_seqs(new_event, current_seq) > 0) return new_event
607
+
608
+ // Find smallest base-10 integer where compare_seqs(int, current_seq) >= 0
609
+ var base = seq_to_int(current_seq)
610
+ return '' + (base + 1 + Math.floor(Math.random() * max_entropy))
611
+ }
612
+
594
613
  function get_event_seq(e) {
595
614
  if (!e) return ''
596
615
 
597
616
  for (let i = e.length - 1; i >= 0; i--)
598
- if (e[i] === '-') return e.slice(i + 1)
617
+ if (e[i] === '-') return i == 0 ? e : e.slice(i + 1)
599
618
  return e
600
619
  }
601
620
 
602
- function increment_seq(s) {
603
- if (!s) return '1'
604
-
605
- let last = s[s.length - 1]
606
- let rest = s.slice(0, -1)
607
-
608
- if (last >= '0' && last <= '8')
609
- return rest + String.fromCharCode(last.charCodeAt(0) + 1)
610
- else
611
- return increment_seq(rest) + '0'
612
- }
613
-
614
- function max_seq(a, b) {
621
+ function compare_seqs(a, b) {
615
622
  if (!a) a = ''
616
623
  if (!b) b = ''
617
624
 
618
- if (compare_seqs(a, b) > 0) return a
619
- return b
620
- }
625
+ var a_neg = a[0] === '-'
626
+ var b_neg = b[0] === '-'
627
+ if (a_neg !== b_neg) return a_neg ? -1 : 1
621
628
 
622
- function compare_seqs(a, b) {
623
- if (!a) a = ''
624
- if (!b) b = ''
629
+ // Both negative: compare magnitudes (reversed)
630
+ if (a_neg) {
631
+ var swap = a.slice(1); a = b.slice(1); b = swap
632
+ }
625
633
 
626
634
  if (a.length !== b.length) return a.length - b.length
627
635
  if (a < b) return -1
@@ -629,6 +637,35 @@ function create_braid_blob() {
629
637
  return 0
630
638
  }
631
639
 
640
+ // Smallest base-10 integer n where compare_seqs(String(n), s) >= 0
641
+ function seq_to_int(s) {
642
+ if (!s || s[0] === '-') return 0
643
+
644
+ var len = s.length
645
+ var min_of_len = Math.pow(10, len - 1) // e.g., len=3 -> 100
646
+ var max_of_len = Math.pow(10, len) - 1 // e.g., len=3 -> 999
647
+
648
+ if (s < String(min_of_len)) return min_of_len
649
+ if (s > String(max_of_len)) return max_of_len + 1
650
+
651
+ // s is in the base-10 range for this length
652
+ // scan for first non-digit > '9', increment prefix and pad zeros
653
+ var n = 0
654
+ for (var i = 0; i < len; i++) {
655
+ var c = s.charCodeAt(i)
656
+ if (c >= 48 && c <= 57) {
657
+ n = n * 10 + (c - 48)
658
+ } else if (c > 57) {
659
+ // non-digit > '9': increment prefix, pad rest with zeros
660
+ return (n + 1) * Math.pow(10, len - i)
661
+ } else {
662
+ // non-digit < '0': just pad rest with zeros
663
+ return n * Math.pow(10, len - i)
664
+ }
665
+ }
666
+ return n
667
+ }
668
+
632
669
  function ascii_ify(s) {
633
670
  return s.replace(/[^\x20-\x7E]/g, c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'))
634
671
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-blob",
3
- "version": "0.0.67",
3
+ "version": "0.0.69",
4
4
  "description": "Library for collaborative blobs over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braid-blob",