braid-blob 0.0.20 → 0.0.22
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/AI-README.md +466 -0
- package/index.js +27 -43
- package/package.json +2 -2
- package/test/tests.js +169 -2
- package/.claude/settings.local.json +0 -11
package/AI-README.md
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
# AI-README.md
|
|
2
|
+
|
|
3
|
+
Machine-optimized documentation for braid-blob library.
|
|
4
|
+
|
|
5
|
+
## DEVELOPMENT_BEST_PRACTICES
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
REPOSITORY: braid-org/braid-blob
|
|
9
|
+
MAIN_BRANCH: master
|
|
10
|
+
NPM_PACKAGE: braid-blob
|
|
11
|
+
|
|
12
|
+
VERSION_BUMP_WORKFLOW:
|
|
13
|
+
1. Make code changes
|
|
14
|
+
2. Update package.json version (increment patch: 0.0.X -> 0.0.X+1)
|
|
15
|
+
3. Git commit with formatted message (see below)
|
|
16
|
+
4. Git push to origin/master
|
|
17
|
+
5. npm publish
|
|
18
|
+
|
|
19
|
+
COMMIT_MESSAGE_FORMAT:
|
|
20
|
+
Pattern: "{version} - {brief description of changes}"
|
|
21
|
+
Style: concise, lowercase, describes what was added/fixed/changed
|
|
22
|
+
Examples:
|
|
23
|
+
- "0.0.20 - adds test.js test runner"
|
|
24
|
+
- "0.0.19 - adds URL support for get/put operations and sync function for bidirectional synchronization"
|
|
25
|
+
- "0.0.18 - fixes meta filename case collision issue on case-insensitive filesystems, updates to url-file-db 0.0.8"
|
|
26
|
+
- "0.0.17 - refactors to use url-file-db, separates meta and blob storage, adds new test infrastructure"
|
|
27
|
+
Footer:
|
|
28
|
+
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
29
|
+
|
|
30
|
+
Co-Authored-By: Claude <noreply@anthropic.com>
|
|
31
|
+
|
|
32
|
+
GIT_WORKFLOW:
|
|
33
|
+
- Always stage all changes: git add -A
|
|
34
|
+
- Commit message uses heredoc for proper formatting
|
|
35
|
+
- Push immediately after commit
|
|
36
|
+
- No feature branches (direct to master)
|
|
37
|
+
- Force-add ignored files if explicitly requested: git add -f
|
|
38
|
+
|
|
39
|
+
NPM_PUBLISH_WORKFLOW:
|
|
40
|
+
- Run after git push completes
|
|
41
|
+
- Command: npm publish
|
|
42
|
+
- No additional flags needed
|
|
43
|
+
- Warnings about repository field normalization are expected and safe
|
|
44
|
+
|
|
45
|
+
TESTING_BEFORE_PUBLISH:
|
|
46
|
+
- Run: npm test (node test/test.js)
|
|
47
|
+
- Or: npm run test:browser (browser-based tests)
|
|
48
|
+
- Tests use local server on port 8889
|
|
49
|
+
- All tests must pass before publishing
|
|
50
|
+
|
|
51
|
+
DEPENDENCY_UPDATES:
|
|
52
|
+
- Update package.json dependency version
|
|
53
|
+
- Mention dependency update in commit message
|
|
54
|
+
- Format: "updates to {package}@{version}"
|
|
55
|
+
- Example: "0.0.18 - ... updates to url-file-db 0.0.8"
|
|
56
|
+
|
|
57
|
+
RESOLVED_ISSUES:
|
|
58
|
+
- url-file-db < 0.0.13: Bug where reading non-existent files returned "index" file contents
|
|
59
|
+
- Fixed in url-file-db 0.0.13+ (properly returns null for non-existent files)
|
|
60
|
+
- Caused "test sync local to remote" to fail with unexpected "shared content"
|
|
61
|
+
- url-file-db 0.0.15+ also relaxed path requirements (no longer requires leading "/")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## MODULE_STRUCTURE
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
EXPORT: create_braid_blob() -> braid_blob_instance
|
|
68
|
+
MODULE_TYPE: CommonJS
|
|
69
|
+
MAIN_ENTRY: index.js
|
|
70
|
+
DEPENDENCIES: [braid-http, url-file-db, fs, path]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## DATA_MODEL
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
braid_blob_instance = {
|
|
77
|
+
// Configuration
|
|
78
|
+
db_folder: string = './braid-blob-db' // blob storage location
|
|
79
|
+
meta_folder: string = './braid-blob-meta' // metadata storage location
|
|
80
|
+
peer: string | null // peer identifier (auto-generated if null)
|
|
81
|
+
|
|
82
|
+
// Runtime state
|
|
83
|
+
cache: object // internal cache
|
|
84
|
+
key_to_subs: Map<key, Map<peer, subscription>> // subscription tracking
|
|
85
|
+
db: url_file_db_instance // blob storage backend
|
|
86
|
+
meta_db: url_file_db_instance // metadata storage backend
|
|
87
|
+
|
|
88
|
+
// Methods
|
|
89
|
+
init: async () -> void
|
|
90
|
+
put: async (key, body, options) -> version_string
|
|
91
|
+
get: async (key, options) -> result_object | null
|
|
92
|
+
serve: async (req, res, options) -> void
|
|
93
|
+
sync: async (a, b, options) -> void
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## VERSION_SYSTEM
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
VERSION_FORMAT: "{peer_id}-{timestamp}"
|
|
101
|
+
EXAMPLE: "abc123xyz-1699564820000"
|
|
102
|
+
COMPARISON_RULES:
|
|
103
|
+
1. Compare numeric length of timestamp
|
|
104
|
+
2. If equal, lexicographic compare timestamp
|
|
105
|
+
3. If equal, lexicographic compare full version string
|
|
106
|
+
MERGE_TYPE: last-write-wins (lww)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## METADATA_SCHEMA
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"event": "peer_id-timestamp",
|
|
114
|
+
"content_type": "mime/type"
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## API_METHODS
|
|
119
|
+
|
|
120
|
+
### put(key, body, options)
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
INPUT:
|
|
124
|
+
key: string | URL
|
|
125
|
+
- string: local key for storage
|
|
126
|
+
- URL: remote endpoint for HTTP PUT
|
|
127
|
+
body: Buffer | string
|
|
128
|
+
options: {
|
|
129
|
+
version?: [string] // explicit version (default: auto-generated)
|
|
130
|
+
content_type?: string // MIME type
|
|
131
|
+
peer?: string // peer identifier
|
|
132
|
+
skip_write?: boolean // skip disk write (for external changes)
|
|
133
|
+
signal?: AbortSignal // for URL mode
|
|
134
|
+
headers?: object // for URL mode
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
OUTPUT: version_string
|
|
138
|
+
|
|
139
|
+
SIDE_EFFECTS:
|
|
140
|
+
- Writes blob to db_folder via url-file-db
|
|
141
|
+
- Writes metadata to meta_folder/db
|
|
142
|
+
- Notifies active subscriptions (except originating peer)
|
|
143
|
+
- If key instanceof URL: makes remote HTTP PUT via braid_fetch
|
|
144
|
+
|
|
145
|
+
VERSION_LOGIC:
|
|
146
|
+
- If options.version provided: use options.version[0]
|
|
147
|
+
- Else: generate "{peer}-{max(Date.now(), last_version_seq+1)}"
|
|
148
|
+
- Only write if new version > existing version (lww)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### get(key, options)
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
INPUT:
|
|
155
|
+
key: string | URL
|
|
156
|
+
- string: local key to retrieve
|
|
157
|
+
- URL: remote endpoint for HTTP GET
|
|
158
|
+
options: {
|
|
159
|
+
subscribe?: callback(update) // enable subscription mode
|
|
160
|
+
header_cb?: callback(result) // called before body read
|
|
161
|
+
before_send_cb?: callback(result) // called before subscription starts
|
|
162
|
+
head?: boolean // HEAD mode (no body)
|
|
163
|
+
parents?: [version] // fork-point for subscriptions
|
|
164
|
+
version?: [version] // request specific version
|
|
165
|
+
peer?: string // peer identifier
|
|
166
|
+
signal?: AbortSignal // for URL mode
|
|
167
|
+
dont_retry?: boolean // for URL mode subscriptions
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
OUTPUT:
|
|
171
|
+
- If subscribe: result_object with unsubscribe callback
|
|
172
|
+
- If URL + subscribe: Response object or Promise rejection
|
|
173
|
+
- If URL + !subscribe: ArrayBuffer
|
|
174
|
+
- If local + !subscribe: {body: Buffer, version: [string], content_type: string}
|
|
175
|
+
- If not found: null
|
|
176
|
+
|
|
177
|
+
result_object: {
|
|
178
|
+
version: [string]
|
|
179
|
+
content_type?: string
|
|
180
|
+
body?: Buffer // only if !subscribe
|
|
181
|
+
unsubscribe?: function // only if subscribe
|
|
182
|
+
sent?: boolean // true if immediate update sent
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
SUBSCRIPTION_BEHAVIOR:
|
|
186
|
+
- Immediate update sent if: no parents OR local_version > parents
|
|
187
|
+
- Future updates sent when put() called
|
|
188
|
+
- Subscription callback receives: {body: Buffer, version: [string], content_type: string}
|
|
189
|
+
|
|
190
|
+
ERROR_HANDLING:
|
|
191
|
+
- Throws "unkown version: {version}" if requested version > local version
|
|
192
|
+
- Returns 309 status code via serve() when version unknown
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### serve(req, res, options)
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
INPUT:
|
|
199
|
+
req: http.IncomingMessage (braidified)
|
|
200
|
+
res: http.ServerResponse (braidified)
|
|
201
|
+
options: {
|
|
202
|
+
key?: string // override URL-based key extraction
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
HTTP_METHOD_MAPPING:
|
|
206
|
+
GET:
|
|
207
|
+
- Calls get() with req.subscribe, req.parents, req.version, req.peer
|
|
208
|
+
- Sets headers: Editable, Accept-Subscribe, Merge-Type, Version/Current-Version
|
|
209
|
+
- Returns 404 if not found
|
|
210
|
+
- Returns 406 if Content-Type not in Accept header
|
|
211
|
+
- Returns 309 if requested version > server version
|
|
212
|
+
|
|
213
|
+
HEAD:
|
|
214
|
+
- Same as GET but no body
|
|
215
|
+
|
|
216
|
+
PUT:
|
|
217
|
+
- Calls put() with body, req.version, req.headers['content-type'], req.peer
|
|
218
|
+
- Returns Version header with new version
|
|
219
|
+
|
|
220
|
+
DELETE:
|
|
221
|
+
- Deletes from db and meta_db
|
|
222
|
+
- Returns 204 No Content
|
|
223
|
+
|
|
224
|
+
OPTIONS:
|
|
225
|
+
- Returns empty response
|
|
226
|
+
|
|
227
|
+
CONCURRENCY:
|
|
228
|
+
- All operations on same key serialized via within_fiber()
|
|
229
|
+
- Different keys process in parallel
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### sync(a, b, options)
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
INPUT:
|
|
236
|
+
a: string | URL // local key or remote endpoint
|
|
237
|
+
b: string | URL // local key or remote endpoint
|
|
238
|
+
options: {
|
|
239
|
+
// options.my_unsync set by function, call to stop sync
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
OUTPUT: void (async, runs indefinitely)
|
|
243
|
+
|
|
244
|
+
BEHAVIOR_MATRIX:
|
|
245
|
+
a=local, b=local:
|
|
246
|
+
- Bidirectional subscription
|
|
247
|
+
- Updates to a -> put(b), updates to b -> put(a)
|
|
248
|
+
|
|
249
|
+
a=local, b=URL (or a=URL, b=local - swapped internally):
|
|
250
|
+
- Fork-point detection via HEAD request with local version
|
|
251
|
+
- If server has local version: subscribe with parents=local_version
|
|
252
|
+
- If server lacks local version: subscribe without parents (full sync)
|
|
253
|
+
- Local changes pushed to remote via PUT
|
|
254
|
+
- Remote changes pulled to local via subscription
|
|
255
|
+
- Auto-reconnect on error with 1 second delay
|
|
256
|
+
- Respects options.my_unsync() for clean shutdown
|
|
257
|
+
|
|
258
|
+
a=URL, b=URL:
|
|
259
|
+
- Bidirectional subscription
|
|
260
|
+
- Updates from a -> put(b), updates from b -> put(a)
|
|
261
|
+
|
|
262
|
+
ERROR_RECOVERY:
|
|
263
|
+
- Remote sync: catches errors, disconnects, retries after 1s
|
|
264
|
+
- Checks 'closed' flag before retry
|
|
265
|
+
- AbortController cleanup on disconnect
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## UTILITY_FUNCTIONS
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
compare_events(a, b) -> -1 | 0 | 1
|
|
272
|
+
Compares version strings by timestamp length, then timestamp value, then full string
|
|
273
|
+
|
|
274
|
+
get_event_seq(event) -> string
|
|
275
|
+
Extracts timestamp portion after last '-'
|
|
276
|
+
|
|
277
|
+
ascii_ify(string) -> string
|
|
278
|
+
Escapes non-ASCII characters as \uXXXX for HTTP headers
|
|
279
|
+
|
|
280
|
+
version_to_header(version_array) -> string
|
|
281
|
+
Converts ["v1", "v2"] -> '"v1", "v2"' for HTTP headers
|
|
282
|
+
|
|
283
|
+
within_fiber(id, func) -> Promise
|
|
284
|
+
Serializes async operations per ID to prevent race conditions
|
|
285
|
+
|
|
286
|
+
slurp(req) -> Promise<Buffer>
|
|
287
|
+
Reads entire HTTP request body
|
|
288
|
+
|
|
289
|
+
isAcceptable(contentType, acceptHeader) -> boolean
|
|
290
|
+
Checks if contentType matches Accept header patterns
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## STORAGE_LAYOUT
|
|
294
|
+
|
|
295
|
+
```
|
|
296
|
+
db_folder/
|
|
297
|
+
{url_file_db structure}
|
|
298
|
+
- Blob data stored via url-file-db
|
|
299
|
+
- Key mapping: URL-safe encoding of keys
|
|
300
|
+
|
|
301
|
+
meta_folder/
|
|
302
|
+
peer.txt # Peer ID (auto-generated if missing)
|
|
303
|
+
db/ # url-file-db for metadata
|
|
304
|
+
{encoded_key}.txt # JSON: {event: version, content_type: mime}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## PROTOCOL_DETAILS
|
|
308
|
+
|
|
309
|
+
```
|
|
310
|
+
HTTP_HEADERS:
|
|
311
|
+
Request:
|
|
312
|
+
Subscribe: true # enable subscription mode
|
|
313
|
+
Version: "v1", "v2" # specific version (for GET)
|
|
314
|
+
Parents: "v1", "v2" # fork-point for subscriptions
|
|
315
|
+
Content-Type: mime/type # for PUT
|
|
316
|
+
Accept: mime/type # for GET (406 if mismatch)
|
|
317
|
+
|
|
318
|
+
Response:
|
|
319
|
+
Version: "v1" # for PUT response
|
|
320
|
+
Current-Version: "v1" # for subscribed GET
|
|
321
|
+
Editable: true
|
|
322
|
+
Accept-Subscribe: true
|
|
323
|
+
Merge-Type: lww
|
|
324
|
+
Content-Type: mime/type
|
|
325
|
+
|
|
326
|
+
STATUS_CODES:
|
|
327
|
+
200 OK # successful GET/PUT
|
|
328
|
+
204 No Content # successful DELETE
|
|
329
|
+
309 Custom # "Version Unknown Here" - requested version not found
|
|
330
|
+
404 Not Found # resource doesn't exist
|
|
331
|
+
406 Not Acceptable # Content-Type not in Accept header
|
|
332
|
+
|
|
333
|
+
BRAID_UPDATE_FORMAT:
|
|
334
|
+
Version: "v1"\r\n
|
|
335
|
+
Merge-Type: lww\r\n
|
|
336
|
+
Content-Length: N\r\n
|
|
337
|
+
\r\n
|
|
338
|
+
{body}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## KEY_BEHAVIORS
|
|
342
|
+
|
|
343
|
+
```
|
|
344
|
+
INITIALIZATION:
|
|
345
|
+
- init() called lazily by put/get/serve
|
|
346
|
+
- init() runs once (subsequent calls return same promise)
|
|
347
|
+
- Creates db and meta_db url-file-db instances
|
|
348
|
+
- Loads or generates peer ID
|
|
349
|
+
|
|
350
|
+
SUBSCRIPTION_MANAGEMENT:
|
|
351
|
+
- key_to_subs: Map<string, Map<string, {sendUpdate}>>
|
|
352
|
+
- Outer key: resource key
|
|
353
|
+
- Inner key: peer identifier
|
|
354
|
+
- Prevents echo: put() doesn't notify originating peer
|
|
355
|
+
- Serialized updates: subscribe_chain ensures sequential callback execution
|
|
356
|
+
|
|
357
|
+
FILE_WATCHING:
|
|
358
|
+
- url-file-db monitors db_folder for external changes
|
|
359
|
+
- External changes trigger put() with skip_write: true
|
|
360
|
+
- Subscriptions notified of external changes
|
|
361
|
+
|
|
362
|
+
CONCURRENCY_CONTROL:
|
|
363
|
+
- within_fiber(key, fn) serializes operations per key
|
|
364
|
+
- Uses promise chain stored in within_fiber.chains[key]
|
|
365
|
+
- Prevents concurrent modification of same resource
|
|
366
|
+
- Automatic cleanup when chain completes
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## INTEGRATION_PATTERNS
|
|
370
|
+
|
|
371
|
+
```
|
|
372
|
+
BASIC_SERVER:
|
|
373
|
+
require('http').createServer((req, res) => {
|
|
374
|
+
braid_blob.serve(req, res)
|
|
375
|
+
}).listen(PORT)
|
|
376
|
+
|
|
377
|
+
CUSTOM_KEY_MAPPING:
|
|
378
|
+
braid_blob.serve(req, res, {
|
|
379
|
+
key: extract_key_from_url(req.url)
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
REMOTE_REPLICATION:
|
|
383
|
+
await braid_blob.sync('local-key', new URL('http://remote/path'))
|
|
384
|
+
|
|
385
|
+
BIDIRECTIONAL_SYNC:
|
|
386
|
+
await braid_blob.sync(
|
|
387
|
+
new URL('http://server1/key'),
|
|
388
|
+
new URL('http://server2/key')
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
PROGRAMMATIC_ACCESS:
|
|
392
|
+
// Local storage
|
|
393
|
+
await braid_blob.put('key', Buffer.from('data'), {version: ['v1']})
|
|
394
|
+
const result = await braid_blob.get('key')
|
|
395
|
+
|
|
396
|
+
// Remote storage
|
|
397
|
+
await braid_blob.put(new URL('http://remote/key'), Buffer.from('data'))
|
|
398
|
+
const data = await braid_blob.get(new URL('http://remote/key'))
|
|
399
|
+
|
|
400
|
+
SUBSCRIPTION_EXAMPLE:
|
|
401
|
+
await braid_blob.get('key', {
|
|
402
|
+
subscribe: async (update) => {
|
|
403
|
+
console.log('Version:', update.version)
|
|
404
|
+
console.log('Body:', update.body.toString())
|
|
405
|
+
}
|
|
406
|
+
})
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## DEPENDENCIES_DETAIL
|
|
410
|
+
|
|
411
|
+
```
|
|
412
|
+
braid-http:
|
|
413
|
+
- http_server (braidify): Adds Braid protocol support to Node.js HTTP
|
|
414
|
+
- fetch (braid_fetch): Braid-aware fetch implementation
|
|
415
|
+
- Handles: Subscribe headers, Version headers, streaming updates
|
|
416
|
+
|
|
417
|
+
url-file-db (^0.0.15):
|
|
418
|
+
- Bidirectional URL ↔ filesystem mapping
|
|
419
|
+
- Collision-resistant encoding (case-insensitive filesystem safe)
|
|
420
|
+
- File watching for external changes
|
|
421
|
+
- Separate instances for blobs (db) and metadata (meta_db)
|
|
422
|
+
- API change in 0.0.15: use get_canonical_path() instead of url_path_to_canonical_path()
|
|
423
|
+
- Fixed in 0.0.13+: properly returns null for non-existent files (not "index" content)
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
## ERROR_CONDITIONS
|
|
427
|
+
|
|
428
|
+
```
|
|
429
|
+
"unkown version: {version}"
|
|
430
|
+
- GET with version/parents newer than local version
|
|
431
|
+
- Results in 309 status via serve()
|
|
432
|
+
|
|
433
|
+
AbortError:
|
|
434
|
+
- sync() handles when AbortController triggered
|
|
435
|
+
- Ignored silently in local→remote sync
|
|
436
|
+
|
|
437
|
+
Connection errors (sync):
|
|
438
|
+
- Caught and trigger reconnect after 1s delay
|
|
439
|
+
- Stops retry if closed flag set
|
|
440
|
+
|
|
441
|
+
File not found:
|
|
442
|
+
- get() returns null for non-existent keys
|
|
443
|
+
- serve() returns 404
|
|
444
|
+
|
|
445
|
+
Content-Type mismatch:
|
|
446
|
+
- serve() returns 406 if Content-Type not in Accept header
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
## TESTING_INFRASTRUCTURE
|
|
450
|
+
|
|
451
|
+
```
|
|
452
|
+
TEST_RUNNER: test/test.js
|
|
453
|
+
- Runs in Node.js or browser
|
|
454
|
+
- Uses /eval endpoint for isolated test execution
|
|
455
|
+
- Browser mode: Opens puppeteer, loads test.html
|
|
456
|
+
|
|
457
|
+
TEST_SUITE: test/tests.js
|
|
458
|
+
- 40+ test cases covering:
|
|
459
|
+
- Basic put/get operations
|
|
460
|
+
- Subscriptions and updates
|
|
461
|
+
- Version conflict resolution
|
|
462
|
+
- Remote operations (URL mode)
|
|
463
|
+
- Sync functionality
|
|
464
|
+
- Error handling
|
|
465
|
+
- Edge cases (version unknown, fork-points)
|
|
466
|
+
```
|
package/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
var {http_server: braidify, fetch: braid_fetch} = require('braid-http'),
|
|
2
2
|
{url_file_db} = require('url-file-db'),
|
|
3
|
-
fs = require('fs'),
|
|
4
3
|
path = require('path')
|
|
5
4
|
|
|
6
5
|
function create_braid_blob() {
|
|
@@ -9,9 +8,8 @@ function create_braid_blob() {
|
|
|
9
8
|
meta_folder: './braid-blob-meta',
|
|
10
9
|
cache: {},
|
|
11
10
|
key_to_subs: {},
|
|
12
|
-
peer: null, //
|
|
13
|
-
db: null
|
|
14
|
-
meta_db: null // url-file-db instance for meta storage
|
|
11
|
+
peer: null, // will be auto-generated if not set by the user
|
|
12
|
+
db: null // url-file-db instance with integrated meta storage
|
|
15
13
|
}
|
|
16
14
|
|
|
17
15
|
braid_blob.init = async () => {
|
|
@@ -21,25 +19,20 @@ function create_braid_blob() {
|
|
|
21
19
|
await braid_blob.init()
|
|
22
20
|
|
|
23
21
|
async function real_init() {
|
|
24
|
-
// Create url-file-db instance
|
|
25
|
-
braid_blob.db = await url_file_db.create(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
// Create url-file-db instance with integrated meta storage
|
|
23
|
+
braid_blob.db = await url_file_db.create(
|
|
24
|
+
braid_blob.db_folder,
|
|
25
|
+
braid_blob.meta_folder,
|
|
26
|
+
async (key) => {
|
|
27
|
+
// File changed externally, notify subscriptions
|
|
28
|
+
var body = await braid_blob.db.read(key)
|
|
29
|
+
await braid_blob.put(key, body, { skip_write: true })
|
|
30
|
+
}
|
|
31
|
+
)
|
|
34
32
|
|
|
35
|
-
// establish a peer id
|
|
36
|
-
if (!braid_blob.peer)
|
|
37
|
-
try {
|
|
38
|
-
braid_blob.peer = await fs.promises.readFile(`${braid_blob.meta_folder}/peer.txt`, 'utf8')
|
|
39
|
-
} catch (e) {}
|
|
33
|
+
// establish a peer id if not already set
|
|
40
34
|
if (!braid_blob.peer)
|
|
41
35
|
braid_blob.peer = Math.random().toString(36).slice(2)
|
|
42
|
-
await fs.promises.writeFile(`${braid_blob.meta_folder}/peer.txt`, braid_blob.peer)
|
|
43
36
|
}
|
|
44
37
|
}
|
|
45
38
|
|
|
@@ -69,11 +62,8 @@ function create_braid_blob() {
|
|
|
69
62
|
|
|
70
63
|
await braid_blob.init()
|
|
71
64
|
|
|
72
|
-
// Read the meta data
|
|
73
|
-
var meta = {}
|
|
74
|
-
var meta_content = await braid_blob.meta_db.read(key)
|
|
75
|
-
if (meta_content)
|
|
76
|
-
meta = JSON.parse(meta_content.toString('utf8'))
|
|
65
|
+
// Read the meta data using new meta API
|
|
66
|
+
var meta = braid_blob.db.get_meta(key) || {}
|
|
77
67
|
|
|
78
68
|
var their_e =
|
|
79
69
|
!options.version ?
|
|
@@ -93,11 +83,12 @@ function create_braid_blob() {
|
|
|
93
83
|
if (!options.skip_write)
|
|
94
84
|
await braid_blob.db.write(key, body)
|
|
95
85
|
|
|
96
|
-
//
|
|
86
|
+
// Update only the fields we want to change in metadata
|
|
87
|
+
var meta_updates = { event: their_e }
|
|
97
88
|
if (options.content_type)
|
|
98
|
-
|
|
89
|
+
meta_updates.content_type = options.content_type
|
|
99
90
|
|
|
100
|
-
await braid_blob.
|
|
91
|
+
await braid_blob.db.update_meta(key, meta_updates)
|
|
101
92
|
|
|
102
93
|
// Notify all subscriptions of the update
|
|
103
94
|
// (except the peer which made the PUT request itself)
|
|
@@ -153,11 +144,8 @@ function create_braid_blob() {
|
|
|
153
144
|
|
|
154
145
|
await braid_blob.init()
|
|
155
146
|
|
|
156
|
-
// Read the meta data
|
|
157
|
-
var meta = {}
|
|
158
|
-
var meta_content = await braid_blob.meta_db.read(key)
|
|
159
|
-
if (meta_content)
|
|
160
|
-
meta = JSON.parse(meta_content.toString('utf8'))
|
|
147
|
+
// Read the meta data using new meta API
|
|
148
|
+
var meta = braid_blob.db.get_meta(key) || {}
|
|
161
149
|
if (meta.event == null) return null
|
|
162
150
|
|
|
163
151
|
var result = {
|
|
@@ -223,7 +211,10 @@ function create_braid_blob() {
|
|
|
223
211
|
braid_blob.serve = async (req, res, options = {}) => {
|
|
224
212
|
await braid_blob.init()
|
|
225
213
|
|
|
226
|
-
if (!options.key)
|
|
214
|
+
if (!options.key) {
|
|
215
|
+
var url = new URL(req.url, 'http://localhost')
|
|
216
|
+
options.key = url_file_db.get_canonical_path(url.pathname)
|
|
217
|
+
}
|
|
227
218
|
|
|
228
219
|
braidify(req, res)
|
|
229
220
|
if (res.is_multiplexer) return
|
|
@@ -235,12 +226,6 @@ function create_braid_blob() {
|
|
|
235
226
|
var body = req.method === 'PUT' && await slurp(req)
|
|
236
227
|
|
|
237
228
|
await within_fiber(options.key, async () => {
|
|
238
|
-
// Read the meta data from meta_db
|
|
239
|
-
var meta = {}
|
|
240
|
-
var meta_content = await braid_blob.meta_db.read(options.key)
|
|
241
|
-
if (meta_content)
|
|
242
|
-
meta = JSON.parse(meta_content.toString('utf8'))
|
|
243
|
-
|
|
244
229
|
if (req.method === 'GET' || req.method === 'HEAD') {
|
|
245
230
|
if (!res.hasHeader("editable")) res.setHeader("Editable", "true")
|
|
246
231
|
if (!req.subscribe) res.setHeader("Accept-Subscribe", "true")
|
|
@@ -298,16 +283,15 @@ function create_braid_blob() {
|
|
|
298
283
|
}
|
|
299
284
|
} else if (req.method === 'PUT') {
|
|
300
285
|
// Handle PUT request to update binary files
|
|
301
|
-
|
|
286
|
+
var event = await braid_blob.put(options.key, body, {
|
|
302
287
|
version: req.version,
|
|
303
288
|
content_type: req.headers['content-type'],
|
|
304
289
|
peer: req.peer
|
|
305
290
|
})
|
|
306
|
-
res.setHeader("Version", version_to_header(
|
|
291
|
+
res.setHeader("Version", version_to_header(event != null ? [event] : []))
|
|
307
292
|
res.end('')
|
|
308
293
|
} else if (req.method === 'DELETE') {
|
|
309
294
|
await braid_blob.db.delete(options.key)
|
|
310
|
-
await braid_blob.meta_db.delete(options.key)
|
|
311
295
|
res.statusCode = 204 // No Content
|
|
312
296
|
res.end('')
|
|
313
297
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "braid-blob",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"description": "Library for collaborative blobs over http using braid.",
|
|
5
5
|
"author": "Braid Working Group",
|
|
6
6
|
"repository": "braid-org/braid-blob",
|
|
@@ -11,6 +11,6 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"braid-http": "~1.3.82",
|
|
14
|
-
"url-file-db": "
|
|
14
|
+
"url-file-db": "^0.0.19"
|
|
15
15
|
}
|
|
16
16
|
}
|
package/test/tests.js
CHANGED
|
@@ -34,7 +34,7 @@ runTest(
|
|
|
34
34
|
)
|
|
35
35
|
|
|
36
36
|
runTest(
|
|
37
|
-
"test that peer is
|
|
37
|
+
"test that peer is different each time we create a new instance",
|
|
38
38
|
async () => {
|
|
39
39
|
var r1 = await braid_fetch(`/eval`, {
|
|
40
40
|
method: 'POST',
|
|
@@ -62,7 +62,7 @@ runTest(
|
|
|
62
62
|
await require('fs').promises.rm(db, { recursive: true, force: true })
|
|
63
63
|
await require('fs').promises.rm(meta, { recursive: true, force: true })
|
|
64
64
|
|
|
65
|
-
res.end('' + (bb1.peer
|
|
65
|
+
res.end('' + (bb1.peer !== bb2.peer))
|
|
66
66
|
})()`
|
|
67
67
|
})
|
|
68
68
|
return await r1.text()
|
|
@@ -100,6 +100,39 @@ runTest(
|
|
|
100
100
|
'test_peer'
|
|
101
101
|
)
|
|
102
102
|
|
|
103
|
+
runTest(
|
|
104
|
+
"test that manually set peer persists through initialization",
|
|
105
|
+
async () => {
|
|
106
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
body: `void (async () => {
|
|
109
|
+
var test_id = 'test-db-' + Math.random().toString(36).slice(2)
|
|
110
|
+
var db = __dirname + '/' + test_id + '-db'
|
|
111
|
+
var meta = __dirname + '/' + test_id + '-meta'
|
|
112
|
+
|
|
113
|
+
// Create instance with manually set peer
|
|
114
|
+
var bb1 = braid_blob.create_braid_blob()
|
|
115
|
+
bb1.db_folder = db
|
|
116
|
+
bb1.meta_folder = meta
|
|
117
|
+
bb1.peer = 'custom-peer-id-123'
|
|
118
|
+
|
|
119
|
+
// Initialize (should keep our custom peer)
|
|
120
|
+
await bb1.init()
|
|
121
|
+
|
|
122
|
+
var peer_after_init = bb1.peer
|
|
123
|
+
|
|
124
|
+
// Clean up
|
|
125
|
+
await require('fs').promises.rm(db, { recursive: true, force: true })
|
|
126
|
+
await require('fs').promises.rm(meta, { recursive: true, force: true })
|
|
127
|
+
|
|
128
|
+
res.end(peer_after_init === 'custom-peer-id-123' ? 'true' : 'false: ' + peer_after_init)
|
|
129
|
+
})()`
|
|
130
|
+
})
|
|
131
|
+
return await r1.text()
|
|
132
|
+
},
|
|
133
|
+
'true'
|
|
134
|
+
)
|
|
135
|
+
|
|
103
136
|
runTest(
|
|
104
137
|
"test that PUTing with shorter event id doesn't do anything.",
|
|
105
138
|
async () => {
|
|
@@ -1142,6 +1175,140 @@ runTest(
|
|
|
1142
1175
|
'309'
|
|
1143
1176
|
)
|
|
1144
1177
|
|
|
1178
|
+
runTest(
|
|
1179
|
+
"test multiple writes preserve correct mtime across restarts",
|
|
1180
|
+
async () => {
|
|
1181
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1182
|
+
method: 'POST',
|
|
1183
|
+
body: `void (async () => {
|
|
1184
|
+
var fs = require('fs').promises
|
|
1185
|
+
var test_id = 'test-multi-write-' + Math.random().toString(36).slice(2)
|
|
1186
|
+
var db_folder = __dirname + '/' + test_id + '-db'
|
|
1187
|
+
var meta_folder = __dirname + '/' + test_id + '-meta'
|
|
1188
|
+
var test_key = 'test-file'
|
|
1189
|
+
|
|
1190
|
+
try {
|
|
1191
|
+
// Create first braid_blob instance
|
|
1192
|
+
var bb1 = braid_blob.create_braid_blob()
|
|
1193
|
+
bb1.db_folder = db_folder
|
|
1194
|
+
bb1.meta_folder = meta_folder
|
|
1195
|
+
|
|
1196
|
+
// First write
|
|
1197
|
+
await bb1.put(test_key, Buffer.from('content1'), {
|
|
1198
|
+
version: ['version-1']
|
|
1199
|
+
})
|
|
1200
|
+
|
|
1201
|
+
// Wait a bit to ensure different mtime
|
|
1202
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
1203
|
+
|
|
1204
|
+
// Second write to same file (this is where the bug would occur)
|
|
1205
|
+
await bb1.put(test_key, Buffer.from('content2'), {
|
|
1206
|
+
version: ['version-2']
|
|
1207
|
+
})
|
|
1208
|
+
|
|
1209
|
+
var result1 = await bb1.get(test_key)
|
|
1210
|
+
|
|
1211
|
+
// Now restart and check
|
|
1212
|
+
var bb2 = braid_blob.create_braid_blob()
|
|
1213
|
+
bb2.db_folder = db_folder
|
|
1214
|
+
bb2.meta_folder = meta_folder
|
|
1215
|
+
|
|
1216
|
+
// This should NOT trigger a file change callback
|
|
1217
|
+
var result2 = await bb2.get(test_key)
|
|
1218
|
+
|
|
1219
|
+
// Version should still be version-2, not regenerated
|
|
1220
|
+
var correct_version = (result2.version[0] === 'version-2')
|
|
1221
|
+
var content_correct = (result2.body.toString() === 'content2')
|
|
1222
|
+
|
|
1223
|
+
// Clean up
|
|
1224
|
+
await fs.rm(db_folder, { recursive: true, force: true })
|
|
1225
|
+
await fs.rm(meta_folder, { recursive: true, force: true })
|
|
1226
|
+
|
|
1227
|
+
res.end(correct_version && content_correct ? 'true' :
|
|
1228
|
+
'false: version=' + result2.version[0] + ', content=' + result2.body.toString())
|
|
1229
|
+
} catch (e) {
|
|
1230
|
+
// Clean up even on error
|
|
1231
|
+
await fs.rm(db_folder, { recursive: true, force: true })
|
|
1232
|
+
await fs.rm(meta_folder, { recursive: true, force: true })
|
|
1233
|
+
res.end('error: ' + e.message)
|
|
1234
|
+
}
|
|
1235
|
+
})()`
|
|
1236
|
+
})
|
|
1237
|
+
return await r1.text()
|
|
1238
|
+
},
|
|
1239
|
+
'true'
|
|
1240
|
+
)
|
|
1241
|
+
|
|
1242
|
+
runTest(
|
|
1243
|
+
"test that files keep same event ID across restarts when not edited",
|
|
1244
|
+
async () => {
|
|
1245
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1246
|
+
method: 'POST',
|
|
1247
|
+
body: `void (async () => {
|
|
1248
|
+
var fs = require('fs').promises
|
|
1249
|
+
var test_id = 'test-persist-event-' + Math.random().toString(36).slice(2)
|
|
1250
|
+
var db_folder = __dirname + '/' + test_id + '-db'
|
|
1251
|
+
var meta_folder = __dirname + '/' + test_id + '-meta'
|
|
1252
|
+
var test_key = 'test-file'
|
|
1253
|
+
var test_content = 'test content that should not change'
|
|
1254
|
+
|
|
1255
|
+
try {
|
|
1256
|
+
// Create first braid_blob instance
|
|
1257
|
+
var bb1 = braid_blob.create_braid_blob()
|
|
1258
|
+
bb1.db_folder = db_folder
|
|
1259
|
+
bb1.meta_folder = meta_folder
|
|
1260
|
+
|
|
1261
|
+
// Put a file with specific version
|
|
1262
|
+
var version1 = await bb1.put(test_key, Buffer.from(test_content), {
|
|
1263
|
+
version: ['test-peer-123456']
|
|
1264
|
+
})
|
|
1265
|
+
|
|
1266
|
+
// Get the file to verify it has the expected version
|
|
1267
|
+
var result1 = await bb1.get(test_key)
|
|
1268
|
+
|
|
1269
|
+
// Check what metadata was saved
|
|
1270
|
+
var meta1 = bb1.db.get_meta(test_key)
|
|
1271
|
+
var debug_info = 'meta1: ' + JSON.stringify(meta1) + '; '
|
|
1272
|
+
|
|
1273
|
+
// Wait a bit to ensure file system has settled
|
|
1274
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
1275
|
+
|
|
1276
|
+
// Now create a second braid_blob instance with the same folders
|
|
1277
|
+
// This simulates a restart
|
|
1278
|
+
var bb2 = braid_blob.create_braid_blob()
|
|
1279
|
+
bb2.db_folder = db_folder
|
|
1280
|
+
bb2.meta_folder = meta_folder
|
|
1281
|
+
|
|
1282
|
+
// Initialize bb2 by doing a get (this triggers init)
|
|
1283
|
+
var result2 = await bb2.get(test_key)
|
|
1284
|
+
|
|
1285
|
+
// Check what metadata bb2 sees
|
|
1286
|
+
var meta2 = bb2.db.get_meta(test_key)
|
|
1287
|
+
debug_info += 'meta2: ' + JSON.stringify(meta2) + '; '
|
|
1288
|
+
|
|
1289
|
+
// The version should be the same - no new event ID generated
|
|
1290
|
+
var versions_match = (result1.version[0] === result2.version[0])
|
|
1291
|
+
var both_have_expected = (result1.version[0] === 'test-peer-123456')
|
|
1292
|
+
|
|
1293
|
+
// Clean up
|
|
1294
|
+
await fs.rm(db_folder, { recursive: true, force: true })
|
|
1295
|
+
await fs.rm(meta_folder, { recursive: true, force: true })
|
|
1296
|
+
|
|
1297
|
+
res.end(versions_match && both_have_expected ? 'true' :
|
|
1298
|
+
'false: v1=' + result1.version[0] + ', v2=' + result2.version[0] + ' | ' + debug_info)
|
|
1299
|
+
} catch (e) {
|
|
1300
|
+
// Clean up even on error
|
|
1301
|
+
await fs.rm(db_folder, { recursive: true, force: true })
|
|
1302
|
+
await fs.rm(meta_folder, { recursive: true, force: true })
|
|
1303
|
+
res.end('error: ' + e.message)
|
|
1304
|
+
}
|
|
1305
|
+
})()`
|
|
1306
|
+
})
|
|
1307
|
+
return await r1.text()
|
|
1308
|
+
},
|
|
1309
|
+
'true'
|
|
1310
|
+
)
|
|
1311
|
+
|
|
1145
1312
|
}
|
|
1146
1313
|
|
|
1147
1314
|
// Export for Node.js (CommonJS)
|