braid-blob 0.0.68 → 0.0.70
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 +30 -17
- package/client.js +50 -23
- package/index.js +59 -22
- package/package.json +1 -1
- package/relative-wallclock-version-type.md +102 -0
- package/spec-for-merge-types.txt +367 -0
- package/spec-for-version-types.txt +1379 -0
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/
|
|
49
|
+
<video src="https://github.com/user-attachments/assets/9416e06b-143a-4b3a-a840-b6484f2571b1" controls width="600"></video>
|
|
50
50
|
|
|
51
51
|
## Network API
|
|
52
52
|
|
|
@@ -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., `"
|
|
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) |
|
|
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: "
|
|
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: "
|
|
103
|
+
Current-Version: "1768467700000"
|
|
104
|
+
Version-Type: relative-wallclock
|
|
102
105
|
|
|
103
106
|
HTTP 200 OK
|
|
104
|
-
Version: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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).
|
|
@@ -157,13 +164,19 @@ HTTP/1.1 200 OK
|
|
|
157
164
|
|
|
158
165
|
Returns `200 OK` even if the blob didn't exist.
|
|
159
166
|
|
|
160
|
-
### Understanding versions
|
|
167
|
+
### Understanding versions and conflict resolution
|
|
161
168
|
|
|
162
|
-
|
|
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
|
+
Braid-blob uses two complementary mechanisms for distributed consistency:
|
|
165
170
|
|
|
166
|
-
|
|
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
|
|
174
|
+
- Versions are compared numerically—larger timestamps are newer
|
|
175
|
+
|
|
176
|
+
**Merge-Type: [`aww`](https://braid.org/protocol/merge-types/aww)** (arbitrary-writer-wins) defines how conflicts are resolved:
|
|
177
|
+
- When two peers make concurrent changes, the version with the higher timestamp wins
|
|
178
|
+
- All peers deterministically converge to the same state without coordination
|
|
179
|
+
- This provides "last-writer-wins" semantics under normal clock conditions
|
|
167
180
|
|
|
168
181
|
|
|
169
182
|
|
|
@@ -192,7 +205,7 @@ var {body, version, content_type} = await braid_blob.get(new URL('https://foo.ba
|
|
|
192
205
|
// Get a specific version of a remote blob:
|
|
193
206
|
var {body} = await braid_blob.get(
|
|
194
207
|
new URL('https://foo.bar/baz'),
|
|
195
|
-
{version: ['
|
|
208
|
+
{version: ['1768467700000']}
|
|
196
209
|
)
|
|
197
210
|
|
|
198
211
|
// To subscribe to a remote blob, without storing updates locally:
|
|
@@ -245,8 +258,8 @@ Retrieves a blob from local storage or a remote URL.
|
|
|
245
258
|
Parameters:
|
|
246
259
|
- `key` - The local blob (if string) or remote URL (if [URL object](https://nodejs.org/api/url.html#class-url)) to read from
|
|
247
260
|
- `params` - Optional configuration object
|
|
248
|
-
- `version` - Retrieve a specific version instead of the latest (e.g., `['
|
|
249
|
-
- `parents` - When subscribing, only receive updates newer than this version (e.g., `['
|
|
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']`)
|
|
250
263
|
- `subscribe` - Callback `(update) => {}` called with each update; `update` has `{body, version, content_type}`
|
|
251
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
|
|
252
265
|
- `content_type` - Expected content type (sent as Accept header for remote URLs)
|
|
@@ -262,7 +275,7 @@ Parameters:
|
|
|
262
275
|
- `key` - The local blob (if string) or remote URL (if [URL object](https://nodejs.org/api/url.html#class-url)) to write to
|
|
263
276
|
- `body` - The data to store (Buffer, ArrayBuffer, or Uint8Array)
|
|
264
277
|
- `params` - Optional configuration object
|
|
265
|
-
- `version` - Specify a version ID for this write (e.g., `['
|
|
278
|
+
- `version` - Specify a version ID for this write (e.g., `['1768467700000']`); if omitted, one is generated automatically
|
|
266
279
|
- `content_type` - MIME type of the blob (e.g., `'image/png'`, `'application/json'`)
|
|
267
280
|
- `signal` - AbortSignal to cancel the request
|
|
268
281
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
112
|
-
if (
|
|
113
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
619
|
-
|
|
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
|
-
|
|
623
|
-
if (
|
|
624
|
-
|
|
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
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Relative-Wallclock Version Type
|
|
2
|
+
|
|
3
|
+
This document describes the **relative-wallclock** version type, which specifies how version identifiers are formatted and interpreted for resources using wallclock-based timestamps.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The relative-wallclock version type uses millisecond timestamps as version identifiers. These timestamps generally correspond to wall-clock time (milliseconds since the Unix epoch), but can advance beyond wall-clock time when necessary to maintain monotonicity.
|
|
8
|
+
|
|
9
|
+
## HTTP Header
|
|
10
|
+
|
|
11
|
+
The relative-wallclock version type is indicated using the `Version-Type` header:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Version-Type: relative-wallclock
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Version Format
|
|
18
|
+
|
|
19
|
+
Versions are numeric strings representing milliseconds since the Unix epoch:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
"1768467700000"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Unlike some versioning schemes that include an agent/peer identifier (e.g., `"alice-1736625600000"`), relative-wallclock versions contain only the timestamp.
|
|
26
|
+
|
|
27
|
+
## Version Generation
|
|
28
|
+
|
|
29
|
+
When creating a new version, the timestamp is calculated as:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
new_version = max(current_time_ms, current_version + random(1, 1000))
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This ensures:
|
|
36
|
+
|
|
37
|
+
1. **Monotonicity**: Versions always increase, even if the clock drifts backward
|
|
38
|
+
2. **Entropy**: A small random number (between 1 and 1000) is added when the clock is behind, reducing collision probability when multiple peers write simultaneously
|
|
39
|
+
3. **Approximate wall-clock correspondence**: Under normal conditions, versions reflect actual time
|
|
40
|
+
|
|
41
|
+
## Comparison Procedure
|
|
42
|
+
|
|
43
|
+
When comparing two versions `a` and `b`:
|
|
44
|
+
|
|
45
|
+
1. **Compare as integers**: Parse the version strings as integers and compare numerically
|
|
46
|
+
2. **Larger wins**: The version with the larger numeric value is considered newer
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
"1768467701000" > "1768467700000" (numerically larger)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Note: Because versions are numeric strings of potentially different lengths, comparison should be done numerically, not lexicographically. However, in practice, millisecond timestamps from the same era will have the same number of digits.
|
|
53
|
+
|
|
54
|
+
## Relationship to Merge Types
|
|
55
|
+
|
|
56
|
+
The relative-wallclock version type specifies only the *format and interpretation* of version identifiers. It does not specify how conflicts are resolved when concurrent versions exist.
|
|
57
|
+
|
|
58
|
+
Conflict resolution is handled by a **Merge Type** (specified via the `Merge-Type` header). For example, the [Arbitrary-Writer-Wins (AWW)](https://braid.org/protocol/merge-types/aww) merge type uses version comparison to deterministically select a winner.
|
|
59
|
+
|
|
60
|
+
When used together:
|
|
61
|
+
- `Version-Type: relative-wallclock` defines how to interpret and compare version strings
|
|
62
|
+
- `Merge-Type: aww` defines that the higher version wins in a conflict
|
|
63
|
+
|
|
64
|
+
## Example
|
|
65
|
+
|
|
66
|
+
```http
|
|
67
|
+
PUT /blob.png HTTP/1.1
|
|
68
|
+
Version: "1768467702000"
|
|
69
|
+
Version-Type: relative-wallclock
|
|
70
|
+
Content-Type: image/png
|
|
71
|
+
Merge-Type: aww
|
|
72
|
+
Content-Length: 34567
|
|
73
|
+
|
|
74
|
+
<binary data>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Response:
|
|
78
|
+
|
|
79
|
+
```http
|
|
80
|
+
HTTP/1.1 200 OK
|
|
81
|
+
Current-Version: "1768467702000"
|
|
82
|
+
Version-Type: relative-wallclock
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Properties
|
|
86
|
+
|
|
87
|
+
- **Simple**: Version identifiers are just numbers
|
|
88
|
+
- **Sortable**: Versions have a natural total ordering
|
|
89
|
+
- **Distributed**: No coordination required between peers to generate versions
|
|
90
|
+
- **Approximate causality**: Generally reflects wall-clock time, providing rough temporal ordering
|
|
91
|
+
- **Collision-resistant**: Random entropy reduces concurrent write collisions
|
|
92
|
+
|
|
93
|
+
## Security Considerations
|
|
94
|
+
|
|
95
|
+
- **Clock manipulation**: A malicious peer could set their clock far in the future to generate versions that always win. Systems should consider rate-limiting or rejecting versions too far in the future.
|
|
96
|
+
- **No authentication**: Version identifiers do not encode who created them. Authentication should be handled at a different layer.
|
|
97
|
+
|
|
98
|
+
## References
|
|
99
|
+
|
|
100
|
+
- [HTTP Resource Versioning](https://github.com/braid-org/braid-spec/blob/master/draft-toomim-httpbis-versions-03.txt)
|
|
101
|
+
- [Merge Types](https://github.com/braid-org/braid-spec/blob/master/draft-toomim-httpbis-merge-types-00.txt)
|
|
102
|
+
- [Arbitrary-Writer-Wins Merge Type](https://braid.org/protocol/merge-types/aww)
|