braid-blob 0.0.70 → 0.0.72

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,7 +46,7 @@ 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/9416e06b-143a-4b3a-a840-b6484f2571b1" controls width="600"></video>
49
+ <video src="https://github.com/user-attachments/assets/24f7be18-87ec-417d-bab5-beee48c5c442" controls width="600"></video>
50
50
 
51
51
  ## Network API
52
52
 
@@ -56,8 +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., `"1768467700000"`) |
60
- | `Version-Type` | How to interpret the structure of version strings (e.g., [`relative-wallclock`](https://braid.org/protocol/version-types/relative-wallclock)); see [Version-Type spec](https://github.com/braid-org/braid-spec/blob/master/draft-toomim-httpbis-versions-03.txt) |
59
+ | `Version` | Unique identifier for this version of the blob (e.g., `"1768467700.000"`) |
60
+ | `Version-Type` | How to interpret the structure of version strings (e.g., [`wallclockish`](https://braid.org/protocol/version-types/wallclockish)); see [Version-Type spec](https://github.com/braid-org/braid-spec/blob/master/draft-toomim-httpbis-versions-03.txt) |
61
61
  | `Parents` | The previous version |
62
62
  | `Merge-Type` | How conflicts resolve consistently (*e.g.* `aww` for [arbitrary-writer-wins](https://braid.org/protocol/merge-types/aww)) |
63
63
  | `Subscribe` | In GET, subscribes client to all future changes |
@@ -74,8 +74,8 @@ Response:
74
74
 
75
75
  ```http
76
76
  HTTP/1.1 200 OK
77
- Version: "1768467700000"
78
- Version-Type: relative-wallclock
77
+ Version: "1768467700.000"
78
+ Version-Type: wallclockish
79
79
  Content-Type: image/png
80
80
  Merge-Type: aww
81
81
  Accept-Subscribe: true
@@ -100,12 +100,12 @@ Response (keeps connection open, streams updates):
100
100
  ```http
101
101
  HTTP/1.1 209 Multiresponse
102
102
  Subscribe: true
103
- Current-Version: "1768467700000"
104
- Version-Type: relative-wallclock
103
+ Current-Version: "1768467700.000"
104
+ Version-Type: wallclockish
105
105
 
106
106
  HTTP 200 OK
107
- Version: "1768467700000"
108
- Version-Type: relative-wallclock
107
+ Version: "1768467700.000"
108
+ Version-Type: wallclockish
109
109
  Content-Type: image/png
110
110
  Merge-Type: aww
111
111
  Content-Length: 12345
@@ -113,8 +113,8 @@ Content-Length: 12345
113
113
  <binary data>
114
114
 
115
115
  HTTP 200 OK
116
- Version: "1768467701000"
117
- Version-Type: relative-wallclock
116
+ Version: "1768467700.100"
117
+ Version-Type: wallclockish
118
118
  Content-Type: image/png
119
119
  Merge-Type: aww
120
120
  Content-Length: 23456
@@ -129,8 +129,8 @@ If the blob doesn't exist yet, `Current-Version` will be blank and no initial up
129
129
 
130
130
  ```http
131
131
  PUT /blob.png HTTP/1.1
132
- Version: "1768467702000"
133
- Version-Type: relative-wallclock
132
+ Version: "1768467700.200"
133
+ Version-Type: wallclockish
134
134
  Content-Type: image/png
135
135
  Merge-Type: aww
136
136
  Content-Length: 34567
@@ -142,8 +142,8 @@ Response:
142
142
 
143
143
  ```http
144
144
  HTTP/1.1 200 OK
145
- Current-Version: "1768467702000"
146
- Version-Type: relative-wallclock
145
+ Current-Version: "1768467700.200"
146
+ Version-Type: wallclockish
147
147
  ```
148
148
 
149
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).
@@ -168,9 +168,9 @@ Returns `200 OK` even if the blob didn't exist.
168
168
 
169
169
  Braid-blob uses two complementary mechanisms for distributed consistency:
170
170
 
171
- **Version-Type: [`relative-wallclock`](https://braid.org/protocol/version-types/relative-wallclock)** defines the format and interpretation of version identifiers:
172
- - Versions are millisecond timestamps (e.g., `"1768467700000"`)
173
- - If the current time is behind the latest known version, a small random number (1-1000) is added to the current version, providing entropy when multiple peers write simultaneously
171
+ **Version-Type: [`wallclockish`](https://braid.org/protocol/version-types/wallclockish)** defines the format and interpretation of version identifiers:
172
+ - Versions are seconds since the epoch with decimal precision (e.g., `"1768467700.000"`)
173
+ - If the current time is behind the latest known version, a small random decimal is added to the current version, providing entropy when multiple peers write simultaneously
174
174
  - Versions are compared numerically—larger timestamps are newer
175
175
 
176
176
  **Merge-Type: [`aww`](https://braid.org/protocol/merge-types/aww)** (arbitrary-writer-wins) defines how conflicts are resolved:
@@ -205,7 +205,7 @@ var {body, version, content_type} = await braid_blob.get(new URL('https://foo.ba
205
205
  // Get a specific version of a remote blob:
206
206
  var {body} = await braid_blob.get(
207
207
  new URL('https://foo.bar/baz'),
208
- {version: ['1768467700000']}
208
+ {version: ['1768467700.000']}
209
209
  )
210
210
 
211
211
  // To subscribe to a remote blob, without storing updates locally:
@@ -258,8 +258,8 @@ Retrieves a blob from local storage or a remote URL.
258
258
  Parameters:
259
259
  - `key` - The local blob (if string) or remote URL (if [URL object](https://nodejs.org/api/url.html#class-url)) to read from
260
260
  - `params` - Optional configuration object
261
- - `version` - Retrieve a specific version instead of the latest (e.g., `['1768467700000']`)
262
- - `parents` - When subscribing, only receive updates newer than this version (e.g., `['1768467700000']`)
261
+ - `version` - Retrieve a specific version instead of the latest (e.g., `['1768467700.000']`)
262
+ - `parents` - When subscribing, only receive updates newer than this version (e.g., `['1768467700.000']`)
263
263
  - `subscribe` - Callback `(update) => {}` called with each update; `update` has `{body, version, content_type}`
264
264
  - `head` - If `true`, returns only metadata (`{version, content_type}`) without the body—useful for checking if a blob exists or getting its current version
265
265
  - `content_type` - Expected content type (sent as Accept header for remote URLs)
@@ -275,7 +275,7 @@ Parameters:
275
275
  - `key` - The local blob (if string) or remote URL (if [URL object](https://nodejs.org/api/url.html#class-url)) to write to
276
276
  - `body` - The data to store (Buffer, ArrayBuffer, or Uint8Array)
277
277
  - `params` - Optional configuration object
278
- - `version` - Specify a version ID for this write (e.g., `['1768467700000']`); if omitted, one is generated automatically
278
+ - `version` - Specify a version ID for this write (e.g., `['1768467700.000']`); if omitted, one is generated automatically
279
279
  - `content_type` - MIME type of the blob (e.g., `'image/png'`, `'application/json'`)
280
280
  - `signal` - AbortSignal to cancel the request
281
281
 
@@ -311,7 +311,7 @@ Parameters:
311
311
 
312
312
  ## Browser Client API
313
313
 
314
- A simple browser client is included for subscribing to blob updates.
314
+ A simple browser client (`client.js`) is included for subscribing to blob updates. Here's how to use it:
315
315
 
316
316
  ```html
317
317
  <script src="https://unpkg.com/braid-http@~1.3/braid-http-client.js"></script>
package/client.js CHANGED
@@ -70,78 +70,69 @@ function braid_blob_client(url, params = {}) {
70
70
  if (!a) a = ''
71
71
  if (!b) b = ''
72
72
 
73
- var c = compare_seqs(get_event_seq(a), get_event_seq(b))
74
- if (c) return c
73
+ // Check if values match wallclockish format
74
+ var re = compare_events.re || (compare_events.re = /^-?[0-9]*\.[0-9]*$/)
75
+ var a_match = re.test(a)
76
+ var b_match = re.test(b)
77
+
78
+ // If only one matches, it wins
79
+ if (a_match && !b_match) return 1
80
+ if (b_match && !a_match) return -1
81
+
82
+ // If neither matches, compare lexicographically
83
+ if (!a_match && !b_match) {
84
+ if (a < b) return -1
85
+ if (a > b) return 1
86
+ return 0
87
+ }
75
88
 
76
- if (a < b) return -1
77
- if (a > b) return 1
78
- return 0
79
- }
89
+ // Both match - compare as decimals using BigInt
90
+ // Add decimal point if missing
91
+ if (a.indexOf('.') === -1) a += '.'
92
+ if (b.indexOf('.') === -1) b += '.'
80
93
 
81
- function create_event(current_event, max_entropy = 1000) {
82
- var new_event = '' + Date.now()
94
+ // Pad the shorter fractional part with zeros
95
+ var diff = (a.length - a.indexOf('.')) - (b.length - b.indexOf('.'))
96
+ if (diff < 0) a += '0'.repeat(-diff)
97
+ else if (diff > 0) b += '0'.repeat(diff)
83
98
 
84
- var current_seq = get_event_seq(current_event)
85
- if (compare_seqs(new_event, current_seq) > 0) return new_event
99
+ // Remove decimal and parse as BigInt
100
+ var a_big = BigInt(a.replace('.', ''))
101
+ var b_big = BigInt(b.replace('.', ''))
86
102
 
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))
103
+ if (a_big < b_big) return -1
104
+ if (a_big > b_big) return 1
105
+ return 0
90
106
  }
91
107
 
92
- function get_event_seq(e) {
93
- if (!e) return ''
108
+ function create_event(current_event, decimal_places=3, entropy_digits=4) {
109
+ var now = '' + Date.now() / 1000
110
+ if (compare_events(now, current_event) > 0)
111
+ return now
94
112
 
95
- for (let i = e.length - 1; i >= 0; i--)
96
- if (e[i] === '-') return i == 0 ? e : e.slice(i + 1)
97
- return e
98
- }
113
+ // Add smallest increment to current_event using BigInt
114
+ var e = current_event || '0'
115
+ if (e.indexOf('.') === -1) e += '.'
99
116
 
100
- function compare_seqs(a, b) {
101
- if (!a) a = ''
102
- if (!b) b = ''
117
+ // Truncate or pad to exactly decimal_places decimal places
118
+ var dot = e.indexOf('.')
119
+ var frac = e.slice(dot + 1)
120
+ if (frac.length > decimal_places) e = e.slice(0, dot + 1 + decimal_places)
121
+ else if (frac.length < decimal_places) e += '0'.repeat(decimal_places - frac.length)
103
122
 
104
- var a_neg = a[0] === '-'
105
- var b_neg = b[0] === '-'
106
- if (a_neg !== b_neg) return a_neg ? -1 : 1
123
+ var big = BigInt(e.replace('.', '')) + 1n
124
+ var str = String(big)
107
125
 
108
- // Both negative: compare magnitudes (reversed)
109
- if (a_neg) {
110
- var swap = a.slice(1); a = b.slice(1); b = swap
111
- }
126
+ // Reinsert decimal point
127
+ var result = str.slice(0, -decimal_places) + '.' + str.slice(-decimal_places)
112
128
 
113
- if (a.length !== b.length) return a.length - b.length
114
- if (a < b) return -1
115
- if (a > b) return 1
116
- return 0
129
+ return result + random_digits(entropy_digits)
117
130
  }
118
131
 
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
132
+ function random_digits(n) {
133
+ if (!n) return ''
134
+ var s = ''
135
+ for (var i = 0; i < n; i++) s += Math.floor(Math.random() * 10)
136
+ return s
146
137
  }
147
138
  }
package/index.js CHANGED
@@ -152,7 +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
+ res.setHeader("Version-Type", "wallclockish")
156
156
  if (result.content_type)
157
157
  res.setHeader('Content-Type', result.content_type)
158
158
  },
@@ -167,7 +167,7 @@ function create_braid_blob() {
167
167
  delete update.content_type
168
168
  }
169
169
  update['Merge-Type'] = 'aww'
170
- update['Version-Type'] = 'relative-wallclock'
170
+ update['Version-Type'] = 'wallclockish'
171
171
  res.sendUpdate(update)
172
172
  } : null
173
173
  })
@@ -206,7 +206,7 @@ function create_braid_blob() {
206
206
  peer: req.peer
207
207
  })
208
208
  res.setHeader("Current-Version", version_to_header(event != null ? [event] : []))
209
- res.setHeader("Version-Type", "relative-wallclock")
209
+ res.setHeader("Version-Type", "wallclockish")
210
210
  res.end('')
211
211
  } else if (req.method === 'DELETE') {
212
212
  await braid_blob.delete(params.key, {
@@ -239,7 +239,7 @@ function create_braid_blob() {
239
239
  'Accept': params.content_type }
240
240
  if (params.version || params.parents)
241
241
  fetch_params.headers = { ...fetch_params.headers,
242
- 'Version-Type': 'relative-wallclock' }
242
+ 'Version-Type': 'wallclockish' }
243
243
 
244
244
  var res = await braid_fetch(key.href, fetch_params)
245
245
 
@@ -353,7 +353,7 @@ function create_braid_blob() {
353
353
  'Content-Type': params.content_type }
354
354
  if (params.version)
355
355
  fetch_params.headers = { ...fetch_params.headers,
356
- 'Version-Type': 'relative-wallclock' }
356
+ 'Version-Type': 'wallclockish' }
357
357
 
358
358
  return await braid_fetch(key.href, fetch_params)
359
359
  }
@@ -591,79 +591,70 @@ function create_braid_blob() {
591
591
  if (!a) a = ''
592
592
  if (!b) b = ''
593
593
 
594
- var c = compare_seqs(get_event_seq(a), get_event_seq(b))
595
- if (c) return c
594
+ // Check if values match wallclockish format
595
+ var re = compare_events.re || (compare_events.re = /^-?[0-9]*\.[0-9]*$/)
596
+ var a_match = re.test(a)
597
+ var b_match = re.test(b)
596
598
 
597
- if (a < b) return -1
598
- if (a > b) return 1
599
- return 0
600
- }
599
+ // If only one matches, it wins
600
+ if (a_match && !b_match) return 1
601
+ if (b_match && !a_match) return -1
601
602
 
602
- function create_event(current_event, max_entropy = 1000) {
603
- var new_event = '' + Date.now()
603
+ // If neither matches, compare lexicographically
604
+ if (!a_match && !b_match) {
605
+ if (a < b) return -1
606
+ if (a > b) return 1
607
+ return 0
608
+ }
604
609
 
605
- var current_seq = get_event_seq(current_event)
606
- if (compare_seqs(new_event, current_seq) > 0) return new_event
610
+ // Both match - compare as decimals using BigInt
611
+ // Add decimal point if missing
612
+ if (a.indexOf('.') === -1) a += '.'
613
+ if (b.indexOf('.') === -1) b += '.'
607
614
 
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
- }
615
+ // Pad the shorter fractional part with zeros
616
+ var diff = (a.length - a.indexOf('.')) - (b.length - b.indexOf('.'))
617
+ if (diff < 0) a += '0'.repeat(-diff)
618
+ else if (diff > 0) b += '0'.repeat(diff)
612
619
 
613
- function get_event_seq(e) {
614
- if (!e) return ''
620
+ // Remove decimal and parse as BigInt
621
+ var a_big = BigInt(a.replace('.', ''))
622
+ var b_big = BigInt(b.replace('.', ''))
615
623
 
616
- for (let i = e.length - 1; i >= 0; i--)
617
- if (e[i] === '-') return i == 0 ? e : e.slice(i + 1)
618
- return e
624
+ if (a_big < b_big) return -1
625
+ if (a_big > b_big) return 1
626
+ return 0
619
627
  }
620
628
 
621
- function compare_seqs(a, b) {
622
- if (!a) a = ''
623
- if (!b) b = ''
629
+ function create_event(current_event, decimal_places=3, entropy_digits=4) {
630
+ var now = '' + Date.now() / 1000
631
+ if (compare_events(now, current_event) > 0)
632
+ return now
624
633
 
625
- var a_neg = a[0] === '-'
626
- var b_neg = b[0] === '-'
627
- if (a_neg !== b_neg) return a_neg ? -1 : 1
634
+ // Add smallest increment to current_event using BigInt
635
+ var e = current_event || '0'
636
+ if (e.indexOf('.') === -1) e += '.'
628
637
 
629
- // Both negative: compare magnitudes (reversed)
630
- if (a_neg) {
631
- var swap = a.slice(1); a = b.slice(1); b = swap
632
- }
638
+ // Truncate or pad to exactly decimal_places decimal places
639
+ var dot = e.indexOf('.')
640
+ var frac = e.slice(dot + 1)
641
+ if (frac.length > decimal_places) e = e.slice(0, dot + 1 + decimal_places)
642
+ else if (frac.length < decimal_places) e += '0'.repeat(decimal_places - frac.length)
633
643
 
634
- if (a.length !== b.length) return a.length - b.length
635
- if (a < b) return -1
636
- if (a > b) return 1
637
- return 0
644
+ var big = BigInt(e.replace('.', '')) + 1n
645
+ var str = String(big)
646
+
647
+ // Reinsert decimal point
648
+ var result = str.slice(0, -decimal_places) + '.' + str.slice(-decimal_places)
649
+
650
+ return result + random_digits(entropy_digits)
638
651
  }
639
652
 
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
653
+ function random_digits(n) {
654
+ if (!n) return ''
655
+ var s = ''
656
+ for (var i = 0; i < n; i++) s += Math.floor(Math.random() * 10)
657
+ return s
667
658
  }
668
659
 
669
660
  function ascii_ify(s) {
@@ -791,7 +782,12 @@ function create_braid_blob() {
791
782
  params.headers.entries() :
792
783
  Object.entries(params.headers))) {
793
784
  var s = normalize_params.special[k.toLowerCase()]
794
- if (s) normalized[s] = v
785
+ if (s) {
786
+ // Parse JSON-encoded header values for version/parents
787
+ if ((s === 'version' || s === 'parents') && typeof v === 'string')
788
+ try { v = JSON.parse(v) } catch (e) {}
789
+ normalized[s] = v
790
+ }
795
791
  else normalized.headers[k] = v
796
792
  }
797
793
  }
package/package.json CHANGED
@@ -1,16 +1,21 @@
1
1
  {
2
2
  "name": "braid-blob",
3
- "version": "0.0.70",
3
+ "version": "0.0.72",
4
4
  "description": "Library for collaborative blobs over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braid-blob",
7
7
  "homepage": "https://braid.org",
8
+ "files": [
9
+ "index.js",
10
+ "client.js",
11
+ "img-live.js"
12
+ ],
8
13
  "scripts": {
9
14
  "test": "node test/test.js",
10
15
  "test:browser": "node test/test.js --browser"
11
16
  },
12
17
  "dependencies": {
13
- "braid-http": "~1.3.87",
18
+ "braid-http": "~1.3.89",
14
19
  "better-sqlite3": "^11.7.0"
15
20
  }
16
21
  }