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.
- package/README.md +98 -74
- package/client.js +50 -23
- package/index.js +59 -22
- 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., `"
|
|
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: "
|
|
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).
|
|
@@ -159,11 +166,9 @@ Returns `200 OK` even if the blob didn't exist.
|
|
|
159
166
|
|
|
160
167
|
### Understanding versions
|
|
161
168
|
|
|
162
|
-
Versions
|
|
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
|
|
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
|
-
###
|
|
186
|
+
### Examples
|
|
182
187
|
|
|
183
|
-
|
|
188
|
+
#### Get a blob
|
|
184
189
|
|
|
185
190
|
```javascript
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
+
### API Reference
|
|
223
245
|
|
|
224
|
-
|
|
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
|
-
|
|
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` -
|
|
243
|
-
- `
|
|
244
|
-
- `subscribe` - Callback
|
|
245
|
-
- `head` - If true
|
|
246
|
-
- `content_type` -
|
|
247
|
-
- `signal` - AbortSignal
|
|
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
|
|
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
|
-
####
|
|
262
|
+
#### braid_blob.put(key, body, params)
|
|
252
263
|
|
|
253
|
-
|
|
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` -
|
|
268
|
+
- `body` - The data to store (Buffer, ArrayBuffer, or Uint8Array)
|
|
262
269
|
- `params` - Optional configuration object
|
|
263
|
-
- `version` -
|
|
264
|
-
- `content_type` -
|
|
265
|
-
- `signal` - AbortSignal
|
|
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
|
-
####
|
|
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
|
-
|
|
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
|
}
|