braid-blob 0.0.20 → 0.0.21

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.
@@ -3,7 +3,16 @@
3
3
  "allow": [
4
4
  "Bash(node test/test.js:*)",
5
5
  "Bash(lsof:*)",
6
- "Bash(xargs kill -9)"
6
+ "Bash(xargs kill -9)",
7
+ "Bash(curl:*)",
8
+ "Bash(cat:*)",
9
+ "Bash(node test.js:*)",
10
+ "Bash(timeout 5 node:*)",
11
+ "Bash(git checkout:*)",
12
+ "Bash(npm show:*)",
13
+ "Bash(npm install:*)",
14
+ "Bash(node:*)",
15
+ "Bash(git add:*)"
7
16
  ],
8
17
  "deny": [],
9
18
  "ask": []
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
@@ -223,7 +223,10 @@ function create_braid_blob() {
223
223
  braid_blob.serve = async (req, res, options = {}) => {
224
224
  await braid_blob.init()
225
225
 
226
- if (!options.key) options.key = url_file_db.get_key(req.url)
226
+ if (!options.key) {
227
+ var url = new URL(req.url, 'http://localhost')
228
+ options.key = url_file_db.get_canonical_path(url.pathname)
229
+ }
227
230
 
228
231
  braidify(req, res)
229
232
  if (res.is_multiplexer) return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-blob",
3
- "version": "0.0.20",
3
+ "version": "0.0.21",
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": "~0.0.8"
14
+ "url-file-db": "^0.0.15"
15
15
  }
16
16
  }