working through somee editor bugs, etc

Orual b95a826f 6f2e5ec3

+2324 -144
+2
Cargo.lock
··· 11788 11788 "jacquard-common", 11789 11789 "jacquard-repo", 11790 11790 "k256", 11791 + "loro", 11791 11792 "miette 7.6.0", 11793 + "mini-moka 0.11.0", 11792 11794 "n0-future 0.1.3", 11793 11795 "rand 0.8.5", 11794 11796 "regex",
+1 -1
Cargo.toml
··· 62 62 # [patch.crates-io] 63 63 # #secp256k1-zkp = { git = "https://github.com/dpc/rust-secp256k1-zkp/", branch = "sanket-pr" } 64 64 # ring = { git = "https://github.com/dpc/ring", rev = "5493e7e76d0d8fb1d3cbb0be9c4944700741b802" } 65 - 65 + loro = "1.9.1" 66 66 67 67 [profile] 68 68
+1 -1
build-workers.sh
··· 5 5 export RUSTFLAGS='--cfg getrandom_backend="wasm_js"' 6 6 cargo build -p weaver-app --bin editor_worker --bin embed_worker \ 7 7 --target wasm32-unknown-unknown --release \ 8 - --no-default-features --features "web","collab-worker" 8 + --no-default-features --features "web","collab-worker","use-index" 9 9 10 10 echo "==> Running wasm-bindgen" 11 11 wasm-bindgen target/wasm32-unknown-unknown/release/editor_worker.wasm \
+1 -1
crates/weaver-app/Dockerfile
··· 32 32 # Build wasm workers 33 33 RUN RUSTFLAGS='--cfg getrandom_backend="wasm_js"' cargo build -p weaver-app --bin editor_worker --bin embed_worker \ 34 34 --target wasm32-unknown-unknown --release \ 35 - --no-default-features --features "web, use-index" 35 + --no-default-features --features "web","collab-worker","use-index" 36 36 37 37 # Run wasm-bindgen on workers 38 38 RUN wasm-bindgen target/wasm32-unknown-unknown/release/editor_worker.wasm \
+642 -8
crates/weaver-app/public/editor_worker.js
··· 17 17 ? { register: () => {}, unregister: () => {} } 18 18 : new FinalizationRegistry(state => state.dtor(state.a, state.b)); 19 19 20 + function debugString(val) { 21 + // primitive types 22 + const type = typeof val; 23 + if (type == 'number' || type == 'boolean' || val == null) { 24 + return `${val}`; 25 + } 26 + if (type == 'string') { 27 + return `"${val}"`; 28 + } 29 + if (type == 'symbol') { 30 + const description = val.description; 31 + if (description == null) { 32 + return 'Symbol'; 33 + } else { 34 + return `Symbol(${description})`; 35 + } 36 + } 37 + if (type == 'function') { 38 + const name = val.name; 39 + if (typeof name == 'string' && name.length > 0) { 40 + return `Function(${name})`; 41 + } else { 42 + return 'Function'; 43 + } 44 + } 45 + // objects 46 + if (Array.isArray(val)) { 47 + const length = val.length; 48 + let debug = '['; 49 + if (length > 0) { 50 + debug += debugString(val[0]); 51 + } 52 + for(let i = 1; i < length; i++) { 53 + debug += ', ' + debugString(val[i]); 54 + } 55 + debug += ']'; 56 + return debug; 57 + } 58 + // Test for built-in 59 + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); 60 + let className; 61 + if (builtInMatches && builtInMatches.length > 1) { 62 + className = builtInMatches[1]; 63 + } else { 64 + // Failed to match the standard '[object ClassName]' 65 + return toString.call(val); 66 + } 67 + if (className == 'Object') { 68 + // we're a user defined class or Object 69 + // JSON.stringify avoids problems with cycles, and is generally much 70 + // easier than looping through ownProperties of `val`. 71 + try { 72 + return 'Object(' + JSON.stringify(val) + ')'; 73 + } catch (_) { 74 + return 'Object'; 75 + } 76 + } 77 + // errors 78 + if (val instanceof Error) { 79 + return `${val.name}: ${val.message}\n${val.stack}`; 80 + } 81 + // TODO we could test for more things here, like `Set`s and `Map`s. 82 + return className; 83 + } 84 + 20 85 function getArrayU8FromWasm0(ptr, len) { 21 86 ptr = ptr >>> 0; 22 87 return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); ··· 167 232 168 233 let WASM_VECTOR_LEN = 0; 169 234 170 - function wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___web_sys_8b37e013ecf136cb___features__gen_MessageEvent__MessageEvent_____(arg0, arg1, arg2) { 171 - wasm.wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___web_sys_8b37e013ecf136cb___features__gen_MessageEvent__MessageEvent_____(arg0, arg1, arg2); 235 + function wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___web_sys_1984c39bba2ffe3a___features__gen_MessageEvent__MessageEvent______1_(arg0, arg1, arg2) { 236 + wasm.wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___web_sys_1984c39bba2ffe3a___features__gen_MessageEvent__MessageEvent______1_(arg0, arg1, arg2); 237 + } 238 + 239 + function wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke_______3_(arg0, arg1) { 240 + wasm.wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke_______3_(arg0, arg1); 241 + } 242 + 243 + function wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___web_sys_1984c39bba2ffe3a___features__gen_MessageEvent__MessageEvent_____(arg0, arg1, arg2) { 244 + wasm.wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___web_sys_1984c39bba2ffe3a___features__gen_MessageEvent__MessageEvent_____(arg0, arg1, arg2); 172 245 } 173 246 174 247 function wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___wasm_bindgen_1add006a0ed82fd3___JsValue_____(arg0, arg1, arg2) { 175 248 wasm.wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___wasm_bindgen_1add006a0ed82fd3___JsValue_____(arg0, arg1, arg2); 176 249 } 177 250 251 + function wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___web_sys_1984c39bba2ffe3a___features__gen_CloseEvent__CloseEvent_____(arg0, arg1, arg2) { 252 + wasm.wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___web_sys_1984c39bba2ffe3a___features__gen_CloseEvent__CloseEvent_____(arg0, arg1, arg2); 253 + } 254 + 255 + function wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke______(arg0, arg1) { 256 + wasm.wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke______(arg0, arg1); 257 + } 258 + 259 + function wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke_______2_(arg0, arg1) { 260 + wasm.wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke_______2_(arg0, arg1); 261 + } 262 + 263 + function wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke_______1_(arg0, arg1) { 264 + wasm.wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke_______1_(arg0, arg1); 265 + } 266 + 267 + function wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___wasm_bindgen_1add006a0ed82fd3___JsValue__wasm_bindgen_1add006a0ed82fd3___JsValue_____(arg0, arg1, arg2, arg3) { 268 + wasm.wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___wasm_bindgen_1add006a0ed82fd3___JsValue__wasm_bindgen_1add006a0ed82fd3___JsValue_____(arg0, arg1, arg2, arg3); 269 + } 270 + 271 + const __wbindgen_enum_BinaryType = ["blob", "arraybuffer"]; 272 + 273 + const __wbindgen_enum_ReadableStreamType = ["bytes"]; 274 + 275 + const __wbindgen_enum_RequestCache = ["default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached"]; 276 + 277 + const __wbindgen_enum_RequestCredentials = ["omit", "same-origin", "include"]; 278 + 279 + const __wbindgen_enum_RequestMode = ["same-origin", "no-cors", "cors", "navigate"]; 280 + 281 + const IntoUnderlyingByteSourceFinalization = (typeof FinalizationRegistry === 'undefined') 282 + ? { register: () => {}, unregister: () => {} } 283 + : new FinalizationRegistry(ptr => wasm.__wbg_intounderlyingbytesource_free(ptr >>> 0, 1)); 284 + 285 + const IntoUnderlyingSinkFinalization = (typeof FinalizationRegistry === 'undefined') 286 + ? { register: () => {}, unregister: () => {} } 287 + : new FinalizationRegistry(ptr => wasm.__wbg_intounderlyingsink_free(ptr >>> 0, 1)); 288 + 289 + const IntoUnderlyingSourceFinalization = (typeof FinalizationRegistry === 'undefined') 290 + ? { register: () => {}, unregister: () => {} } 291 + : new FinalizationRegistry(ptr => wasm.__wbg_intounderlyingsource_free(ptr >>> 0, 1)); 292 + 178 293 const JSOwnerFinalization = (typeof FinalizationRegistry === 'undefined') 179 294 ? { register: () => {}, unregister: () => {} } 180 295 : new FinalizationRegistry(ptr => wasm.__wbg_jsowner_free(ptr >>> 0, 1)); 181 296 297 + class IntoUnderlyingByteSource { 298 + __destroy_into_raw() { 299 + const ptr = this.__wbg_ptr; 300 + this.__wbg_ptr = 0; 301 + IntoUnderlyingByteSourceFinalization.unregister(this); 302 + return ptr; 303 + } 304 + free() { 305 + const ptr = this.__destroy_into_raw(); 306 + wasm.__wbg_intounderlyingbytesource_free(ptr, 0); 307 + } 308 + /** 309 + * @returns {number} 310 + */ 311 + get autoAllocateChunkSize() { 312 + const ret = wasm.intounderlyingbytesource_autoAllocateChunkSize(this.__wbg_ptr); 313 + return ret >>> 0; 314 + } 315 + /** 316 + * @param {ReadableByteStreamController} controller 317 + * @returns {Promise<any>} 318 + */ 319 + pull(controller) { 320 + const ret = wasm.intounderlyingbytesource_pull(this.__wbg_ptr, controller); 321 + return ret; 322 + } 323 + /** 324 + * @param {ReadableByteStreamController} controller 325 + */ 326 + start(controller) { 327 + wasm.intounderlyingbytesource_start(this.__wbg_ptr, controller); 328 + } 329 + /** 330 + * @returns {ReadableStreamType} 331 + */ 332 + get type() { 333 + const ret = wasm.intounderlyingbytesource_type(this.__wbg_ptr); 334 + return __wbindgen_enum_ReadableStreamType[ret]; 335 + } 336 + cancel() { 337 + const ptr = this.__destroy_into_raw(); 338 + wasm.intounderlyingbytesource_cancel(ptr); 339 + } 340 + } 341 + if (Symbol.dispose) IntoUnderlyingByteSource.prototype[Symbol.dispose] = IntoUnderlyingByteSource.prototype.free; 342 + __exports.IntoUnderlyingByteSource = IntoUnderlyingByteSource; 343 + 344 + class IntoUnderlyingSink { 345 + __destroy_into_raw() { 346 + const ptr = this.__wbg_ptr; 347 + this.__wbg_ptr = 0; 348 + IntoUnderlyingSinkFinalization.unregister(this); 349 + return ptr; 350 + } 351 + free() { 352 + const ptr = this.__destroy_into_raw(); 353 + wasm.__wbg_intounderlyingsink_free(ptr, 0); 354 + } 355 + /** 356 + * @param {any} reason 357 + * @returns {Promise<any>} 358 + */ 359 + abort(reason) { 360 + const ptr = this.__destroy_into_raw(); 361 + const ret = wasm.intounderlyingsink_abort(ptr, reason); 362 + return ret; 363 + } 364 + /** 365 + * @returns {Promise<any>} 366 + */ 367 + close() { 368 + const ptr = this.__destroy_into_raw(); 369 + const ret = wasm.intounderlyingsink_close(ptr); 370 + return ret; 371 + } 372 + /** 373 + * @param {any} chunk 374 + * @returns {Promise<any>} 375 + */ 376 + write(chunk) { 377 + const ret = wasm.intounderlyingsink_write(this.__wbg_ptr, chunk); 378 + return ret; 379 + } 380 + } 381 + if (Symbol.dispose) IntoUnderlyingSink.prototype[Symbol.dispose] = IntoUnderlyingSink.prototype.free; 382 + __exports.IntoUnderlyingSink = IntoUnderlyingSink; 383 + 384 + class IntoUnderlyingSource { 385 + __destroy_into_raw() { 386 + const ptr = this.__wbg_ptr; 387 + this.__wbg_ptr = 0; 388 + IntoUnderlyingSourceFinalization.unregister(this); 389 + return ptr; 390 + } 391 + free() { 392 + const ptr = this.__destroy_into_raw(); 393 + wasm.__wbg_intounderlyingsource_free(ptr, 0); 394 + } 395 + /** 396 + * @param {ReadableStreamDefaultController} controller 397 + * @returns {Promise<any>} 398 + */ 399 + pull(controller) { 400 + const ret = wasm.intounderlyingsource_pull(this.__wbg_ptr, controller); 401 + return ret; 402 + } 403 + cancel() { 404 + const ptr = this.__destroy_into_raw(); 405 + wasm.intounderlyingsource_cancel(ptr); 406 + } 407 + } 408 + if (Symbol.dispose) IntoUnderlyingSource.prototype[Symbol.dispose] = IntoUnderlyingSource.prototype.free; 409 + __exports.IntoUnderlyingSource = IntoUnderlyingSource; 410 + 182 411 class JSOwner { 183 412 __destroy_into_raw() { 184 413 const ptr = this.__wbg_ptr; ··· 229 458 function __wbg_get_imports() { 230 459 const imports = {}; 231 460 imports.wbg = {}; 461 + imports.wbg.__wbg___wbindgen_boolean_get_dea25b33882b895b = function(arg0) { 462 + const v = arg0; 463 + const ret = typeof(v) === 'boolean' ? v : undefined; 464 + return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0; 465 + }; 466 + imports.wbg.__wbg___wbindgen_debug_string_adfb662ae34724b6 = function(arg0, arg1) { 467 + const ret = debugString(arg1); 468 + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 469 + const len1 = WASM_VECTOR_LEN; 470 + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); 471 + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); 472 + }; 232 473 imports.wbg.__wbg___wbindgen_is_function_8d400b8b1af978cd = function(arg0) { 233 474 const ret = typeof(arg0) === 'function'; 234 475 return ret; ··· 246 487 const ret = arg0 === undefined; 247 488 return ret; 248 489 }; 490 + imports.wbg.__wbg___wbindgen_string_get_a2a31e16edf96e42 = function(arg0, arg1) { 491 + const obj = arg1; 492 + const ret = typeof(obj) === 'string' ? obj : undefined; 493 + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 494 + var len1 = WASM_VECTOR_LEN; 495 + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); 496 + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); 497 + }; 249 498 imports.wbg.__wbg___wbindgen_throw_dd24417ed36fc46e = function(arg0, arg1) { 250 499 throw new Error(getStringFromWasm0(arg0, arg1)); 251 500 }; 252 501 imports.wbg.__wbg__wbg_cb_unref_87dfb5aaa0cbcea7 = function(arg0) { 253 502 arg0._wbg_cb_unref(); 254 503 }; 504 + imports.wbg.__wbg_abort_07646c894ebbf2bd = function(arg0) { 505 + arg0.abort(); 506 + }; 507 + imports.wbg.__wbg_abort_399ecbcfd6ef3c8e = function(arg0, arg1) { 508 + arg0.abort(arg1); 509 + }; 510 + imports.wbg.__wbg_addEventListener_e792423147a80626 = function() { return handleError(function (arg0, arg1, arg2, arg3) { 511 + arg0.addEventListener(getStringFromWasm0(arg1, arg2), arg3); 512 + }, arguments) }; 513 + imports.wbg.__wbg_append_c5cbdf46455cc776 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { 514 + arg0.append(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); 515 + }, arguments) }; 516 + imports.wbg.__wbg_arrayBuffer_c04af4fce566092d = function() { return handleError(function (arg0) { 517 + const ret = arg0.arrayBuffer(); 518 + return ret; 519 + }, arguments) }; 520 + imports.wbg.__wbg_body_947b901c33f7fe32 = function(arg0) { 521 + const ret = arg0.body; 522 + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); 523 + }; 524 + imports.wbg.__wbg_buffer_6cb2fecb1f253d71 = function(arg0) { 525 + const ret = arg0.buffer; 526 + return ret; 527 + }; 528 + imports.wbg.__wbg_byobRequest_f8e3517f5f8ad284 = function(arg0) { 529 + const ret = arg0.byobRequest; 530 + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); 531 + }; 532 + imports.wbg.__wbg_byteLength_faa9938885bdeee6 = function(arg0) { 533 + const ret = arg0.byteLength; 534 + return ret; 535 + }; 536 + imports.wbg.__wbg_byteOffset_3868b6a19ba01dea = function(arg0) { 537 + const ret = arg0.byteOffset; 538 + return ret; 539 + }; 255 540 imports.wbg.__wbg_call_3020136f7a2d6e44 = function() { return handleError(function (arg0, arg1, arg2) { 256 541 const ret = arg0.call(arg1, arg2); 257 542 return ret; ··· 260 545 const ret = arg0.call(arg1); 261 546 return ret; 262 547 }, arguments) }; 548 + imports.wbg.__wbg_cancel_a65cf45dca50ba4c = function(arg0) { 549 + const ret = arg0.cancel(); 550 + return ret; 551 + }; 552 + imports.wbg.__wbg_catch_b9db41d97d42bd02 = function(arg0, arg1) { 553 + const ret = arg0.catch(arg1); 554 + return ret; 555 + }; 556 + imports.wbg.__wbg_clearTimeout_b716ecb44bea14ed = function(arg0) { 557 + const ret = clearTimeout(arg0); 558 + return ret; 559 + }; 560 + imports.wbg.__wbg_clearTimeout_f7a6c75a3d228439 = function() { return handleError(function (arg0, arg1) { 561 + arg0.clearTimeout(arg1); 562 + }, arguments) }; 563 + imports.wbg.__wbg_close_0af5661bf3d335f2 = function() { return handleError(function (arg0) { 564 + arg0.close(); 565 + }, arguments) }; 263 566 imports.wbg.__wbg_close_0b472ca2d13f54f7 = function(arg0) { 264 567 arg0.close(); 265 568 }; 569 + imports.wbg.__wbg_close_1db3952de1b5b1cf = function() { return handleError(function (arg0) { 570 + arg0.close(); 571 + }, arguments) }; 572 + imports.wbg.__wbg_close_3ec111e7b23d94d8 = function() { return handleError(function (arg0) { 573 + arg0.close(); 574 + }, arguments) }; 575 + imports.wbg.__wbg_code_85a811fe6ca962be = function(arg0) { 576 + const ret = arg0.code; 577 + return ret; 578 + }; 579 + imports.wbg.__wbg_code_c2a85f2863ec11b3 = function(arg0) { 580 + const ret = arg0.code; 581 + return ret; 582 + }; 266 583 imports.wbg.__wbg_crypto_574e78ad8b13b65f = function(arg0) { 267 584 const ret = arg0.crypto; 268 585 return ret; ··· 271 588 const ret = arg0.data; 272 589 return ret; 273 590 }; 591 + imports.wbg.__wbg_done_62ea16af4ce34b24 = function(arg0) { 592 + const ret = arg0.done; 593 + return ret; 594 + }; 595 + imports.wbg.__wbg_enqueue_a7e6b1ee87963aad = function() { return handleError(function (arg0, arg1) { 596 + arg0.enqueue(arg1); 597 + }, arguments) }; 274 598 imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function(arg0, arg1) { 275 599 let deferred0_0; 276 600 let deferred0_1; ··· 282 606 wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); 283 607 } 284 608 }; 609 + imports.wbg.__wbg_fetch_7fb7602a1bf647ec = function(arg0) { 610 + const ret = fetch(arg0); 611 + return ret; 612 + }; 613 + imports.wbg.__wbg_fetch_90447c28cc0b095e = function(arg0, arg1) { 614 + const ret = arg0.fetch(arg1); 615 + return ret; 616 + }; 617 + imports.wbg.__wbg_getRandomValues_1c61fac11405ffdc = function() { return handleError(function (arg0, arg1) { 618 + globalThis.crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1)); 619 + }, arguments) }; 285 620 imports.wbg.__wbg_getRandomValues_b8f5dbd5f3995a9e = function() { return handleError(function (arg0, arg1) { 286 621 arg0.getRandomValues(arg1); 287 622 }, arguments) }; 623 + imports.wbg.__wbg_getReader_48e00749fe3f6089 = function() { return handleError(function (arg0) { 624 + const ret = arg0.getReader(); 625 + return ret; 626 + }, arguments) }; 627 + imports.wbg.__wbg_get_af9dab7e9603ea93 = function() { return handleError(function (arg0, arg1) { 628 + const ret = Reflect.get(arg0, arg1); 629 + return ret; 630 + }, arguments) }; 631 + imports.wbg.__wbg_get_done_f98a6e62c4e18fb9 = function(arg0) { 632 + const ret = arg0.done; 633 + return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0; 634 + }; 635 + imports.wbg.__wbg_get_value_63e39884ef11812e = function(arg0) { 636 + const ret = arg0.value; 637 + return ret; 638 + }; 639 + imports.wbg.__wbg_has_0e670569d65d3a45 = function() { return handleError(function (arg0, arg1) { 640 + const ret = Reflect.has(arg0, arg1); 641 + return ret; 642 + }, arguments) }; 643 + imports.wbg.__wbg_headers_654c30e1bcccc552 = function(arg0) { 644 + const ret = arg0.headers; 645 + return ret; 646 + }; 647 + imports.wbg.__wbg_instanceof_ArrayBuffer_f3320d2419cd0355 = function(arg0) { 648 + let result; 649 + try { 650 + result = arg0 instanceof ArrayBuffer; 651 + } catch (_) { 652 + result = false; 653 + } 654 + const ret = result; 655 + return ret; 656 + }; 657 + imports.wbg.__wbg_instanceof_Blob_e9c51ce33a4b6181 = function(arg0) { 658 + let result; 659 + try { 660 + result = arg0 instanceof Blob; 661 + } catch (_) { 662 + result = false; 663 + } 664 + const ret = result; 665 + return ret; 666 + }; 667 + imports.wbg.__wbg_instanceof_Response_cd74d1c2ac92cb0b = function(arg0) { 668 + let result; 669 + try { 670 + result = arg0 instanceof Response; 671 + } catch (_) { 672 + result = false; 673 + } 674 + const ret = result; 675 + return ret; 676 + }; 288 677 imports.wbg.__wbg_instanceof_Window_b5cf7783caa68180 = function(arg0) { 289 678 let result; 290 679 try { ··· 293 682 result = false; 294 683 } 295 684 const ret = result; 685 + return ret; 686 + }; 687 + imports.wbg.__wbg_iterator_27b7c8b35ab3e86b = function() { 688 + const ret = Symbol.iterator; 296 689 return ret; 297 690 }; 298 691 imports.wbg.__wbg_length_22ac23eaec9d8053 = function(arg0) { ··· 340 733 wasm.__wbindgen_free(deferred1_0, deferred1_1, 1); 341 734 } 342 735 }, arguments) }; 736 + imports.wbg.__wbg_message_a4e9a39ee8f92b17 = function(arg0, arg1) { 737 + const ret = arg1.message; 738 + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 739 + const len1 = WASM_VECTOR_LEN; 740 + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); 741 + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); 742 + }; 343 743 imports.wbg.__wbg_msCrypto_a61aeb35a24c1329 = function(arg0) { 344 744 const ret = arg0.msCrypto; 345 745 return ret; 346 746 }; 747 + imports.wbg.__wbg_new_1ba21ce319a06297 = function() { 748 + const ret = new Object(); 749 + return ret; 750 + }; 751 + imports.wbg.__wbg_new_25f239778d6112b9 = function() { 752 + const ret = new Array(); 753 + return ret; 754 + }; 755 + imports.wbg.__wbg_new_3c79b3bb1b32b7d3 = function() { return handleError(function () { 756 + const ret = new Headers(); 757 + return ret; 758 + }, arguments) }; 759 + imports.wbg.__wbg_new_6421f6084cc5bc5a = function(arg0) { 760 + const ret = new Uint8Array(arg0); 761 + return ret; 762 + }; 763 + imports.wbg.__wbg_new_7c30d1f874652e62 = function() { return handleError(function (arg0, arg1) { 764 + const ret = new WebSocket(getStringFromWasm0(arg0, arg1)); 765 + return ret; 766 + }, arguments) }; 767 + imports.wbg.__wbg_new_881a222c65f168fc = function() { return handleError(function () { 768 + const ret = new AbortController(); 769 + return ret; 770 + }, arguments) }; 347 771 imports.wbg.__wbg_new_8a6f238a6ece86ea = function() { 348 772 const ret = new Error(); 349 773 return ret; 350 774 }; 775 + imports.wbg.__wbg_new_df1173567d5ff028 = function(arg0, arg1) { 776 + const ret = new Error(getStringFromWasm0(arg0, arg1)); 777 + return ret; 778 + }; 779 + imports.wbg.__wbg_new_ff12d2b041fb48f1 = function(arg0, arg1) { 780 + try { 781 + var state0 = {a: arg0, b: arg1}; 782 + var cb0 = (arg0, arg1) => { 783 + const a = state0.a; 784 + state0.a = 0; 785 + try { 786 + return wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___wasm_bindgen_1add006a0ed82fd3___JsValue__wasm_bindgen_1add006a0ed82fd3___JsValue_____(a, state0.b, arg0, arg1); 787 + } finally { 788 + state0.a = a; 789 + } 790 + }; 791 + const ret = new Promise(cb0); 792 + return ret; 793 + } finally { 794 + state0.a = state0.b = 0; 795 + } 796 + }; 351 797 imports.wbg.__wbg_new_from_slice_f9c22b9153b26992 = function(arg0, arg1) { 352 798 const ret = new Uint8Array(getArrayU8FromWasm0(arg0, arg1)); 353 799 return ret; ··· 356 802 const ret = new Function(getStringFromWasm0(arg0, arg1)); 357 803 return ret; 358 804 }; 805 + imports.wbg.__wbg_new_with_byte_offset_and_length_d85c3da1fd8df149 = function(arg0, arg1, arg2) { 806 + const ret = new Uint8Array(arg0, arg1 >>> 0, arg2 >>> 0); 807 + return ret; 808 + }; 359 809 imports.wbg.__wbg_new_with_length_aa5eaf41d35235e5 = function(arg0) { 360 810 const ret = new Uint8Array(arg0 >>> 0); 361 811 return ret; 362 812 }; 813 + imports.wbg.__wbg_new_with_str_and_init_c5748f76f5108934 = function() { return handleError(function (arg0, arg1, arg2) { 814 + const ret = new Request(getStringFromWasm0(arg0, arg1), arg2); 815 + return ret; 816 + }, arguments) }; 817 + imports.wbg.__wbg_new_with_str_sequence_073466a4a5387941 = function() { return handleError(function (arg0, arg1, arg2) { 818 + const ret = new WebSocket(getStringFromWasm0(arg0, arg1), arg2); 819 + return ret; 820 + }, arguments) }; 821 + imports.wbg.__wbg_next_138a17bbf04e926c = function(arg0) { 822 + const ret = arg0.next; 823 + return ret; 824 + }; 825 + imports.wbg.__wbg_next_3cfe5c0fe2a4cc53 = function() { return handleError(function (arg0) { 826 + const ret = arg0.next(); 827 + return ret; 828 + }, arguments) }; 363 829 imports.wbg.__wbg_node_905d3e251edff8a2 = function(arg0) { 364 830 const ret = arg0.node; 831 + return ret; 832 + }; 833 + imports.wbg.__wbg_now_2c95c9de01293173 = function(arg0) { 834 + const ret = arg0.now(); 835 + return ret; 836 + }; 837 + imports.wbg.__wbg_now_69d776cd24f5215b = function() { 838 + const ret = Date.now(); 365 839 return ret; 366 840 }; 367 841 imports.wbg.__wbg_now_8cf15d6e317793e1 = function(arg0) { ··· 372 846 const ret = Date.now(); 373 847 return ret; 374 848 }; 849 + imports.wbg.__wbg_performance_7a3ffd0b17f663ad = function(arg0) { 850 + const ret = arg0.performance; 851 + return ret; 852 + }; 375 853 imports.wbg.__wbg_performance_c77a440eff2efd9b = function(arg0) { 376 854 const ret = arg0.performance; 377 855 return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); ··· 386 864 imports.wbg.__wbg_prototypesetcall_dfe9b766cdc1f1fd = function(arg0, arg1, arg2) { 387 865 Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2); 388 866 }; 867 + imports.wbg.__wbg_push_7d9be8f38fc13975 = function(arg0, arg1) { 868 + const ret = arg0.push(arg1); 869 + return ret; 870 + }; 389 871 imports.wbg.__wbg_queueMicrotask_9b549dfce8865860 = function(arg0) { 390 872 const ret = arg0.queueMicrotask; 391 873 return ret; ··· 396 878 imports.wbg.__wbg_randomFillSync_ac0988aba3254290 = function() { return handleError(function (arg0, arg1) { 397 879 arg0.randomFillSync(arg1); 398 880 }, arguments) }; 881 + imports.wbg.__wbg_read_39c4b35efcd03c25 = function(arg0) { 882 + const ret = arg0.read(); 883 + return ret; 884 + }; 885 + imports.wbg.__wbg_readyState_9d0976dcad561aa9 = function(arg0) { 886 + const ret = arg0.readyState; 887 + return ret; 888 + }; 889 + imports.wbg.__wbg_reason_d4eb9e40592438c2 = function(arg0, arg1) { 890 + const ret = arg1.reason; 891 + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 892 + const len1 = WASM_VECTOR_LEN; 893 + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); 894 + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); 895 + }; 896 + imports.wbg.__wbg_releaseLock_a5912f590b185180 = function(arg0) { 897 + arg0.releaseLock(); 898 + }; 899 + imports.wbg.__wbg_removeEventListener_54bf92f4a849bd7d = function() { return handleError(function (arg0, arg1, arg2, arg3) { 900 + arg0.removeEventListener(getStringFromWasm0(arg1, arg2), arg3); 901 + }, arguments) }; 399 902 imports.wbg.__wbg_require_60cc747a6bc5215a = function() { return handleError(function () { 400 903 const ret = module.require; 401 904 return ret; ··· 404 907 const ret = Promise.resolve(arg0); 405 908 return ret; 406 909 }; 910 + imports.wbg.__wbg_respond_9f7fc54636c4a3af = function() { return handleError(function (arg0, arg1) { 911 + arg0.respond(arg1 >>> 0); 912 + }, arguments) }; 913 + imports.wbg.__wbg_send_7cc36bb628044281 = function() { return handleError(function (arg0, arg1, arg2) { 914 + arg0.send(getStringFromWasm0(arg1, arg2)); 915 + }, arguments) }; 916 + imports.wbg.__wbg_send_ea59e150ab5ebe08 = function() { return handleError(function (arg0, arg1, arg2) { 917 + arg0.send(getArrayU8FromWasm0(arg1, arg2)); 918 + }, arguments) }; 919 + imports.wbg.__wbg_setTimeout_4302406184dcc5be = function(arg0, arg1) { 920 + const ret = setTimeout(arg0, arg1); 921 + return ret; 922 + }; 923 + imports.wbg.__wbg_setTimeout_ceaa8eadc563d26e = function() { return handleError(function (arg0, arg1, arg2) { 924 + const ret = arg0.setTimeout(arg1, arg2); 925 + return ret; 926 + }, arguments) }; 927 + imports.wbg.__wbg_set_169e13b608078b7b = function(arg0, arg1, arg2) { 928 + arg0.set(getArrayU8FromWasm0(arg1, arg2)); 929 + }; 930 + imports.wbg.__wbg_set_binaryType_73e8c75df97825f8 = function(arg0, arg1) { 931 + arg0.binaryType = __wbindgen_enum_BinaryType[arg1]; 932 + }; 933 + imports.wbg.__wbg_set_body_8e743242d6076a4f = function(arg0, arg1) { 934 + arg0.body = arg1; 935 + }; 936 + imports.wbg.__wbg_set_cache_0e437c7c8e838b9b = function(arg0, arg1) { 937 + arg0.cache = __wbindgen_enum_RequestCache[arg1]; 938 + }; 939 + imports.wbg.__wbg_set_credentials_55ae7c3c106fd5be = function(arg0, arg1) { 940 + arg0.credentials = __wbindgen_enum_RequestCredentials[arg1]; 941 + }; 942 + imports.wbg.__wbg_set_handle_event_14baa3949ef6909d = function(arg0, arg1) { 943 + arg0.handleEvent = arg1; 944 + }; 945 + imports.wbg.__wbg_set_headers_5671cf088e114d2b = function(arg0, arg1) { 946 + arg0.headers = arg1; 947 + }; 948 + imports.wbg.__wbg_set_method_76c69e41b3570627 = function(arg0, arg1, arg2) { 949 + arg0.method = getStringFromWasm0(arg1, arg2); 950 + }; 951 + imports.wbg.__wbg_set_mode_611016a6818fc690 = function(arg0, arg1) { 952 + arg0.mode = __wbindgen_enum_RequestMode[arg1]; 953 + }; 954 + imports.wbg.__wbg_set_onclose_032729b3d7ed7a9e = function(arg0, arg1) { 955 + arg0.onclose = arg1; 956 + }; 957 + imports.wbg.__wbg_set_onerror_7819daa6af176ddb = function(arg0, arg1) { 958 + arg0.onerror = arg1; 959 + }; 407 960 imports.wbg.__wbg_set_onmessage_5fe29d0fb54cb575 = function(arg0, arg1) { 408 961 arg0.onmessage = arg1; 409 962 }; 963 + imports.wbg.__wbg_set_onmessage_71321d0bed69856c = function(arg0, arg1) { 964 + arg0.onmessage = arg1; 965 + }; 966 + imports.wbg.__wbg_set_onopen_6d4abedb27ba5656 = function(arg0, arg1) { 967 + arg0.onopen = arg1; 968 + }; 969 + imports.wbg.__wbg_set_signal_e89be862d0091009 = function(arg0, arg1) { 970 + arg0.signal = arg1; 971 + }; 972 + imports.wbg.__wbg_signal_3c14fbdc89694b39 = function(arg0) { 973 + const ret = arg0.signal; 974 + return ret; 975 + }; 410 976 imports.wbg.__wbg_stack_0ed75d68575b0f3c = function(arg0, arg1) { 411 977 const ret = arg1.stack; 412 978 const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); ··· 430 996 const ret = typeof window === 'undefined' ? null : window; 431 997 return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); 432 998 }; 999 + imports.wbg.__wbg_status_9bfc680efca4bdfd = function(arg0) { 1000 + const ret = arg0.status; 1001 + return ret; 1002 + }; 1003 + imports.wbg.__wbg_stringify_655a6390e1f5eb6b = function() { return handleError(function (arg0) { 1004 + const ret = JSON.stringify(arg0); 1005 + return ret; 1006 + }, arguments) }; 433 1007 imports.wbg.__wbg_subarray_845f2f5bce7d061a = function(arg0, arg1, arg2) { 434 1008 const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0); 435 1009 return ret; 436 1010 }; 1011 + imports.wbg.__wbg_then_429f7caf1026411d = function(arg0, arg1, arg2) { 1012 + const ret = arg0.then(arg1, arg2); 1013 + return ret; 1014 + }; 437 1015 imports.wbg.__wbg_then_4f95312d68691235 = function(arg0, arg1) { 438 1016 const ret = arg0.then(arg1); 439 1017 return ret; 440 1018 }; 1019 + imports.wbg.__wbg_url_b6d11838a4f95198 = function(arg0, arg1) { 1020 + const ret = arg1.url; 1021 + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 1022 + const len1 = WASM_VECTOR_LEN; 1023 + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); 1024 + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); 1025 + }; 1026 + imports.wbg.__wbg_url_df28eef824b04410 = function(arg0, arg1) { 1027 + const ret = arg1.url; 1028 + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 1029 + const len1 = WASM_VECTOR_LEN; 1030 + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); 1031 + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); 1032 + }; 1033 + imports.wbg.__wbg_value_57b7b035e117f7ee = function(arg0) { 1034 + const ret = arg0.value; 1035 + return ret; 1036 + }; 441 1037 imports.wbg.__wbg_versions_c01dfd4722a88165 = function(arg0) { 442 1038 const ret = arg0.versions; 443 1039 return ret; 444 1040 }; 1041 + imports.wbg.__wbg_view_788aaf149deefd2f = function(arg0) { 1042 + const ret = arg0.view; 1043 + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); 1044 + }; 1045 + imports.wbg.__wbg_wasClean_4154a2d59fdb4dd7 = function(arg0) { 1046 + const ret = arg0.wasClean; 1047 + return ret; 1048 + }; 445 1049 imports.wbg.__wbindgen_cast_2241b6af4c4b2941 = function(arg0, arg1) { 446 1050 // Cast intrinsic for `Ref(String) -> Externref`. 447 1051 const ret = getStringFromWasm0(arg0, arg1); 448 1052 return ret; 449 1053 }; 450 - imports.wbg.__wbindgen_cast_3fda284bdcf7704e = function(arg0, arg1) { 451 - // Cast intrinsic for `Closure(Closure { dtor_idx: 941, function: Function { arguments: [Externref], shim_idx: 942, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. 452 - const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen_1add006a0ed82fd3___closure__destroy___dyn_core_b125d98f3949a913___ops__function__FnMut__wasm_bindgen_1add006a0ed82fd3___JsValue____Output_______, wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___wasm_bindgen_1add006a0ed82fd3___JsValue_____); 1054 + imports.wbg.__wbindgen_cast_4d50504c7960c7db = function(arg0, arg1) { 1055 + // Cast intrinsic for `Closure(Closure { dtor_idx: 5089, function: Function { arguments: [], shim_idx: 5090, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. 1056 + const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen_1add006a0ed82fd3___closure__destroy___dyn_core_b125d98f3949a913___ops__function__FnMut_____Output________2_, wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke_______3_); 453 1057 return ret; 454 1058 }; 455 - imports.wbg.__wbindgen_cast_56c1ebb4e8528c2a = function(arg0, arg1) { 456 - // Cast intrinsic for `Closure(Closure { dtor_idx: 132, function: Function { arguments: [NamedExternref("MessageEvent")], shim_idx: 133, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`. 457 - const ret = makeClosure(arg0, arg1, wasm.wasm_bindgen_1add006a0ed82fd3___closure__destroy___dyn_core_b125d98f3949a913___ops__function__Fn__web_sys_8b37e013ecf136cb___features__gen_MessageEvent__MessageEvent____Output_______, wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___web_sys_8b37e013ecf136cb___features__gen_MessageEvent__MessageEvent_____); 1059 + imports.wbg.__wbindgen_cast_4f4fadff60f85046 = function(arg0, arg1) { 1060 + // Cast intrinsic for `Closure(Closure { dtor_idx: 4976, function: Function { arguments: [], shim_idx: 4977, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. 1061 + const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen_1add006a0ed82fd3___closure__destroy___dyn_core_b125d98f3949a913___ops__function__FnMut_____Output________1_, wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke_______2_); 1062 + return ret; 1063 + }; 1064 + imports.wbg.__wbindgen_cast_75c82a975d6b40cd = function(arg0, arg1) { 1065 + // Cast intrinsic for `Closure(Closure { dtor_idx: 5269, function: Function { arguments: [Externref], shim_idx: 5270, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. 1066 + const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen_1add006a0ed82fd3___closure__destroy___dyn_core_b125d98f3949a913___ops__function__FnMut__wasm_bindgen_1add006a0ed82fd3___JsValue____Output_______, wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___wasm_bindgen_1add006a0ed82fd3___JsValue_____); 458 1067 return ret; 459 1068 }; 460 1069 imports.wbg.__wbindgen_cast_cb9088102bce6b30 = function(arg0, arg1) { 461 1070 // Cast intrinsic for `Ref(Slice(U8)) -> NamedExternref("Uint8Array")`. 462 1071 const ret = getArrayU8FromWasm0(arg0, arg1); 1072 + return ret; 1073 + }; 1074 + imports.wbg.__wbindgen_cast_ccbfef0f15d2f2b2 = function(arg0, arg1) { 1075 + // Cast intrinsic for `Closure(Closure { dtor_idx: 3405, function: Function { arguments: [NamedExternref("CloseEvent")], shim_idx: 3406, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. 1076 + const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen_1add006a0ed82fd3___closure__destroy___dyn_core_b125d98f3949a913___ops__function__FnMut__web_sys_1984c39bba2ffe3a___features__gen_CloseEvent__CloseEvent____Output_______, wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___web_sys_1984c39bba2ffe3a___features__gen_CloseEvent__CloseEvent_____); 1077 + return ret; 1078 + }; 1079 + imports.wbg.__wbindgen_cast_d086004c1192dab2 = function(arg0, arg1) { 1080 + // Cast intrinsic for `Closure(Closure { dtor_idx: 3461, function: Function { arguments: [], shim_idx: 3462, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`. 1081 + const ret = makeClosure(arg0, arg1, wasm.wasm_bindgen_1add006a0ed82fd3___closure__destroy___dyn_core_b125d98f3949a913___ops__function__Fn_____Output_______, wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke_______1_); 1082 + return ret; 1083 + }; 1084 + imports.wbg.__wbindgen_cast_d9e01159fa505611 = function(arg0, arg1) { 1085 + // Cast intrinsic for `Closure(Closure { dtor_idx: 3438, function: Function { arguments: [], shim_idx: 3439, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. 1086 + const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen_1add006a0ed82fd3___closure__destroy___dyn_core_b125d98f3949a913___ops__function__FnMut_____Output_______, wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke______); 1087 + return ret; 1088 + }; 1089 + imports.wbg.__wbindgen_cast_ec4f04da93f4f5b8 = function(arg0, arg1) { 1090 + // Cast intrinsic for `Closure(Closure { dtor_idx: 3959, function: Function { arguments: [NamedExternref("MessageEvent")], shim_idx: 3960, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. 1091 + const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen_1add006a0ed82fd3___closure__destroy___dyn_core_b125d98f3949a913___ops__function__FnMut__web_sys_1984c39bba2ffe3a___features__gen_MessageEvent__MessageEvent____Output_______, wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___web_sys_1984c39bba2ffe3a___features__gen_MessageEvent__MessageEvent______1_); 1092 + return ret; 1093 + }; 1094 + imports.wbg.__wbindgen_cast_f7b350875fdc9e82 = function(arg0, arg1) { 1095 + // Cast intrinsic for `Closure(Closure { dtor_idx: 145, function: Function { arguments: [NamedExternref("MessageEvent")], shim_idx: 146, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`. 1096 + const ret = makeClosure(arg0, arg1, wasm.wasm_bindgen_1add006a0ed82fd3___closure__destroy___dyn_core_b125d98f3949a913___ops__function__Fn__web_sys_1984c39bba2ffe3a___features__gen_MessageEvent__MessageEvent____Output_______, wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___web_sys_1984c39bba2ffe3a___features__gen_MessageEvent__MessageEvent_____); 463 1097 return ret; 464 1098 }; 465 1099 imports.wbg.__wbindgen_init_externref_table = function() {
+217 -28
crates/weaver-app/public/embed_worker.js
··· 232 232 233 233 let WASM_VECTOR_LEN = 0; 234 234 235 - function wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke_______1_(arg0, arg1) { 236 - wasm.wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke_______1_(arg0, arg1); 235 + function wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___web_sys_1984c39bba2ffe3a___features__gen_MessageEvent__MessageEvent_____(arg0, arg1, arg2) { 236 + wasm.wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___web_sys_1984c39bba2ffe3a___features__gen_MessageEvent__MessageEvent_____(arg0, arg1, arg2); 237 237 } 238 238 239 - function wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___web_sys_8b37e013ecf136cb___features__gen_MessageEvent__MessageEvent_____(arg0, arg1, arg2) { 240 - wasm.wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___web_sys_8b37e013ecf136cb___features__gen_MessageEvent__MessageEvent_____(arg0, arg1, arg2); 239 + function wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke_______1_(arg0, arg1) { 240 + wasm.wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke_______1_(arg0, arg1); 241 241 } 242 242 243 243 function wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke______(arg0, arg1) { ··· 248 248 wasm.wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___wasm_bindgen_1add006a0ed82fd3___JsValue_____(arg0, arg1, arg2); 249 249 } 250 250 251 + function wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___wasm_bindgen_1add006a0ed82fd3___JsValue__wasm_bindgen_1add006a0ed82fd3___JsValue_____(arg0, arg1, arg2, arg3) { 252 + wasm.wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___wasm_bindgen_1add006a0ed82fd3___JsValue__wasm_bindgen_1add006a0ed82fd3___JsValue_____(arg0, arg1, arg2, arg3); 253 + } 254 + 255 + const __wbindgen_enum_ReadableStreamType = ["bytes"]; 256 + 251 257 const __wbindgen_enum_RequestCache = ["default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached"]; 252 258 253 259 const __wbindgen_enum_RequestCredentials = ["omit", "same-origin", "include"]; 254 260 255 261 const __wbindgen_enum_RequestMode = ["same-origin", "no-cors", "cors", "navigate"]; 256 262 263 + const IntoUnderlyingByteSourceFinalization = (typeof FinalizationRegistry === 'undefined') 264 + ? { register: () => {}, unregister: () => {} } 265 + : new FinalizationRegistry(ptr => wasm.__wbg_intounderlyingbytesource_free(ptr >>> 0, 1)); 266 + 267 + const IntoUnderlyingSinkFinalization = (typeof FinalizationRegistry === 'undefined') 268 + ? { register: () => {}, unregister: () => {} } 269 + : new FinalizationRegistry(ptr => wasm.__wbg_intounderlyingsink_free(ptr >>> 0, 1)); 270 + 271 + const IntoUnderlyingSourceFinalization = (typeof FinalizationRegistry === 'undefined') 272 + ? { register: () => {}, unregister: () => {} } 273 + : new FinalizationRegistry(ptr => wasm.__wbg_intounderlyingsource_free(ptr >>> 0, 1)); 274 + 257 275 const JSOwnerFinalization = (typeof FinalizationRegistry === 'undefined') 258 276 ? { register: () => {}, unregister: () => {} } 259 277 : new FinalizationRegistry(ptr => wasm.__wbg_jsowner_free(ptr >>> 0, 1)); 260 278 279 + class IntoUnderlyingByteSource { 280 + __destroy_into_raw() { 281 + const ptr = this.__wbg_ptr; 282 + this.__wbg_ptr = 0; 283 + IntoUnderlyingByteSourceFinalization.unregister(this); 284 + return ptr; 285 + } 286 + free() { 287 + const ptr = this.__destroy_into_raw(); 288 + wasm.__wbg_intounderlyingbytesource_free(ptr, 0); 289 + } 290 + /** 291 + * @returns {number} 292 + */ 293 + get autoAllocateChunkSize() { 294 + const ret = wasm.intounderlyingbytesource_autoAllocateChunkSize(this.__wbg_ptr); 295 + return ret >>> 0; 296 + } 297 + /** 298 + * @param {ReadableByteStreamController} controller 299 + * @returns {Promise<any>} 300 + */ 301 + pull(controller) { 302 + const ret = wasm.intounderlyingbytesource_pull(this.__wbg_ptr, controller); 303 + return ret; 304 + } 305 + /** 306 + * @param {ReadableByteStreamController} controller 307 + */ 308 + start(controller) { 309 + wasm.intounderlyingbytesource_start(this.__wbg_ptr, controller); 310 + } 311 + /** 312 + * @returns {ReadableStreamType} 313 + */ 314 + get type() { 315 + const ret = wasm.intounderlyingbytesource_type(this.__wbg_ptr); 316 + return __wbindgen_enum_ReadableStreamType[ret]; 317 + } 318 + cancel() { 319 + const ptr = this.__destroy_into_raw(); 320 + wasm.intounderlyingbytesource_cancel(ptr); 321 + } 322 + } 323 + if (Symbol.dispose) IntoUnderlyingByteSource.prototype[Symbol.dispose] = IntoUnderlyingByteSource.prototype.free; 324 + __exports.IntoUnderlyingByteSource = IntoUnderlyingByteSource; 325 + 326 + class IntoUnderlyingSink { 327 + __destroy_into_raw() { 328 + const ptr = this.__wbg_ptr; 329 + this.__wbg_ptr = 0; 330 + IntoUnderlyingSinkFinalization.unregister(this); 331 + return ptr; 332 + } 333 + free() { 334 + const ptr = this.__destroy_into_raw(); 335 + wasm.__wbg_intounderlyingsink_free(ptr, 0); 336 + } 337 + /** 338 + * @param {any} reason 339 + * @returns {Promise<any>} 340 + */ 341 + abort(reason) { 342 + const ptr = this.__destroy_into_raw(); 343 + const ret = wasm.intounderlyingsink_abort(ptr, reason); 344 + return ret; 345 + } 346 + /** 347 + * @returns {Promise<any>} 348 + */ 349 + close() { 350 + const ptr = this.__destroy_into_raw(); 351 + const ret = wasm.intounderlyingsink_close(ptr); 352 + return ret; 353 + } 354 + /** 355 + * @param {any} chunk 356 + * @returns {Promise<any>} 357 + */ 358 + write(chunk) { 359 + const ret = wasm.intounderlyingsink_write(this.__wbg_ptr, chunk); 360 + return ret; 361 + } 362 + } 363 + if (Symbol.dispose) IntoUnderlyingSink.prototype[Symbol.dispose] = IntoUnderlyingSink.prototype.free; 364 + __exports.IntoUnderlyingSink = IntoUnderlyingSink; 365 + 366 + class IntoUnderlyingSource { 367 + __destroy_into_raw() { 368 + const ptr = this.__wbg_ptr; 369 + this.__wbg_ptr = 0; 370 + IntoUnderlyingSourceFinalization.unregister(this); 371 + return ptr; 372 + } 373 + free() { 374 + const ptr = this.__destroy_into_raw(); 375 + wasm.__wbg_intounderlyingsource_free(ptr, 0); 376 + } 377 + /** 378 + * @param {ReadableStreamDefaultController} controller 379 + * @returns {Promise<any>} 380 + */ 381 + pull(controller) { 382 + const ret = wasm.intounderlyingsource_pull(this.__wbg_ptr, controller); 383 + return ret; 384 + } 385 + cancel() { 386 + const ptr = this.__destroy_into_raw(); 387 + wasm.intounderlyingsource_cancel(ptr); 388 + } 389 + } 390 + if (Symbol.dispose) IntoUnderlyingSource.prototype[Symbol.dispose] = IntoUnderlyingSource.prototype.free; 391 + __exports.IntoUnderlyingSource = IntoUnderlyingSource; 392 + 261 393 class JSOwner { 262 394 __destroy_into_raw() { 263 395 const ptr = this.__wbg_ptr; ··· 355 487 const ret = arg0.arrayBuffer(); 356 488 return ret; 357 489 }, arguments) }; 490 + imports.wbg.__wbg_buffer_6cb2fecb1f253d71 = function(arg0) { 491 + const ret = arg0.buffer; 492 + return ret; 493 + }; 494 + imports.wbg.__wbg_byobRequest_f8e3517f5f8ad284 = function(arg0) { 495 + const ret = arg0.byobRequest; 496 + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); 497 + }; 498 + imports.wbg.__wbg_byteLength_faa9938885bdeee6 = function(arg0) { 499 + const ret = arg0.byteLength; 500 + return ret; 501 + }; 502 + imports.wbg.__wbg_byteOffset_3868b6a19ba01dea = function(arg0) { 503 + const ret = arg0.byteOffset; 504 + return ret; 505 + }; 506 + imports.wbg.__wbg_call_3020136f7a2d6e44 = function() { return handleError(function (arg0, arg1, arg2) { 507 + const ret = arg0.call(arg1, arg2); 508 + return ret; 509 + }, arguments) }; 358 510 imports.wbg.__wbg_call_abb4ff46ce38be40 = function() { return handleError(function (arg0, arg1) { 359 511 const ret = arg0.call(arg1); 360 512 return ret; ··· 362 514 imports.wbg.__wbg_clearTimeout_15dfc3d1dcb635c6 = function() { return handleError(function (arg0, arg1) { 363 515 arg0.clearTimeout(arg1); 364 516 }, arguments) }; 365 - imports.wbg.__wbg_clearTimeout_3b6a9a13d4bde075 = function(arg0) { 517 + imports.wbg.__wbg_clearTimeout_b716ecb44bea14ed = function(arg0) { 366 518 const ret = clearTimeout(arg0); 367 519 return ret; 368 520 }; 521 + imports.wbg.__wbg_close_0af5661bf3d335f2 = function() { return handleError(function (arg0) { 522 + arg0.close(); 523 + }, arguments) }; 369 524 imports.wbg.__wbg_close_0b472ca2d13f54f7 = function(arg0) { 370 525 arg0.close(); 371 526 }; 527 + imports.wbg.__wbg_close_3ec111e7b23d94d8 = function() { return handleError(function (arg0) { 528 + arg0.close(); 529 + }, arguments) }; 372 530 imports.wbg.__wbg_data_8bf4ae669a78a688 = function(arg0) { 373 531 const ret = arg0.data; 374 532 return ret; ··· 377 535 const ret = arg0.done; 378 536 return ret; 379 537 }; 538 + imports.wbg.__wbg_enqueue_a7e6b1ee87963aad = function() { return handleError(function (arg0, arg1) { 539 + arg0.enqueue(arg1); 540 + }, arguments) }; 380 541 imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function(arg0, arg1) { 381 542 let deferred0_0; 382 543 let deferred0_1; ··· 388 549 wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); 389 550 } 390 551 }; 391 - imports.wbg.__wbg_fetch_90447c28cc0b095e = function(arg0, arg1) { 392 - const ret = arg0.fetch(arg1); 393 - return ret; 394 - }; 395 - imports.wbg.__wbg_fetch_df3fa17a5772dafb = function(arg0) { 552 + imports.wbg.__wbg_fetch_7fb7602a1bf647ec = function(arg0) { 396 553 const ret = fetch(arg0); 397 554 return ret; 398 555 }; 399 - imports.wbg.__wbg_getTime_ad1e9878a735af08 = function(arg0) { 400 - const ret = arg0.getTime(); 556 + imports.wbg.__wbg_fetch_90447c28cc0b095e = function(arg0, arg1) { 557 + const ret = arg0.fetch(arg1); 401 558 return ret; 402 559 }; 403 560 imports.wbg.__wbg_get_af9dab7e9603ea93 = function() { return handleError(function (arg0, arg1) { ··· 440 597 const ret = arg0.length; 441 598 return ret; 442 599 }; 443 - imports.wbg.__wbg_new_0_23cedd11d9b40c9d = function() { 444 - const ret = new Date(); 445 - return ret; 446 - }; 447 600 imports.wbg.__wbg_new_1ba21ce319a06297 = function() { 448 601 const ret = new Object(); 449 602 return ret; ··· 464 617 const ret = new Error(); 465 618 return ret; 466 619 }; 620 + imports.wbg.__wbg_new_df1173567d5ff028 = function(arg0, arg1) { 621 + const ret = new Error(getStringFromWasm0(arg0, arg1)); 622 + return ret; 623 + }; 624 + imports.wbg.__wbg_new_ff12d2b041fb48f1 = function(arg0, arg1) { 625 + try { 626 + var state0 = {a: arg0, b: arg1}; 627 + var cb0 = (arg0, arg1) => { 628 + const a = state0.a; 629 + state0.a = 0; 630 + try { 631 + return wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___wasm_bindgen_1add006a0ed82fd3___JsValue__wasm_bindgen_1add006a0ed82fd3___JsValue_____(a, state0.b, arg0, arg1); 632 + } finally { 633 + state0.a = a; 634 + } 635 + }; 636 + const ret = new Promise(cb0); 637 + return ret; 638 + } finally { 639 + state0.a = state0.b = 0; 640 + } 641 + }; 467 642 imports.wbg.__wbg_new_from_slice_f9c22b9153b26992 = function(arg0, arg1) { 468 643 const ret = new Uint8Array(getArrayU8FromWasm0(arg0, arg1)); 469 644 return ret; 470 645 }; 471 646 imports.wbg.__wbg_new_no_args_cb138f77cf6151ee = function(arg0, arg1) { 472 647 const ret = new Function(getStringFromWasm0(arg0, arg1)); 648 + return ret; 649 + }; 650 + imports.wbg.__wbg_new_with_byte_offset_and_length_d85c3da1fd8df149 = function(arg0, arg1, arg2) { 651 + const ret = new Uint8Array(arg0, arg1 >>> 0, arg2 >>> 0); 473 652 return ret; 474 653 }; 475 654 imports.wbg.__wbg_new_with_str_and_init_c5748f76f5108934 = function() { return handleError(function (arg0, arg1, arg2) { ··· 517 696 const ret = Promise.resolve(arg0); 518 697 return ret; 519 698 }; 520 - imports.wbg.__wbg_setTimeout_35a07631c669fbee = function(arg0, arg1) { 699 + imports.wbg.__wbg_respond_9f7fc54636c4a3af = function() { return handleError(function (arg0, arg1) { 700 + arg0.respond(arg1 >>> 0); 701 + }, arguments) }; 702 + imports.wbg.__wbg_setTimeout_4302406184dcc5be = function(arg0, arg1) { 521 703 const ret = setTimeout(arg0, arg1); 522 704 return ret; 523 705 }; ··· 525 707 const ret = arg0.setTimeout(arg1, arg2); 526 708 return ret; 527 709 }, arguments) }; 710 + imports.wbg.__wbg_set_169e13b608078b7b = function(arg0, arg1, arg2) { 711 + arg0.set(getArrayU8FromWasm0(arg1, arg2)); 712 + }; 528 713 imports.wbg.__wbg_set_body_8e743242d6076a4f = function(arg0, arg1) { 529 714 arg0.body = arg1; 530 715 }; ··· 607 792 const ret = arg0.value; 608 793 return ret; 609 794 }; 610 - imports.wbg.__wbindgen_cast_067e3c0104867449 = function(arg0, arg1) { 611 - // Cast intrinsic for `Closure(Closure { dtor_idx: 225, function: Function { arguments: [NamedExternref("MessageEvent")], shim_idx: 226, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`. 612 - const ret = makeClosure(arg0, arg1, wasm.wasm_bindgen_1add006a0ed82fd3___closure__destroy___dyn_core_b125d98f3949a913___ops__function__Fn__web_sys_8b37e013ecf136cb___features__gen_MessageEvent__MessageEvent____Output_______, wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___web_sys_8b37e013ecf136cb___features__gen_MessageEvent__MessageEvent_____); 613 - return ret; 795 + imports.wbg.__wbg_view_788aaf149deefd2f = function(arg0) { 796 + const ret = arg0.view; 797 + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); 614 798 }; 615 799 imports.wbg.__wbindgen_cast_2241b6af4c4b2941 = function(arg0, arg1) { 616 800 // Cast intrinsic for `Ref(String) -> Externref`. 617 801 const ret = getStringFromWasm0(arg0, arg1); 618 802 return ret; 619 803 }; 620 - imports.wbg.__wbindgen_cast_4c40eebfb345262b = function(arg0, arg1) { 621 - // Cast intrinsic for `Closure(Closure { dtor_idx: 1169, function: Function { arguments: [], shim_idx: 1170, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. 622 - const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen_1add006a0ed82fd3___closure__destroy___dyn_core_b125d98f3949a913___ops__function__FnMut_____Output_______, wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke______); 804 + imports.wbg.__wbindgen_cast_6e929a04fb6adb86 = function(arg0, arg1) { 805 + // Cast intrinsic for `Closure(Closure { dtor_idx: 389, function: Function { arguments: [NamedExternref("MessageEvent")], shim_idx: 390, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`. 806 + const ret = makeClosure(arg0, arg1, wasm.wasm_bindgen_1add006a0ed82fd3___closure__destroy___dyn_core_b125d98f3949a913___ops__function__Fn__web_sys_1984c39bba2ffe3a___features__gen_MessageEvent__MessageEvent____Output_______, wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___web_sys_1984c39bba2ffe3a___features__gen_MessageEvent__MessageEvent_____); 623 807 return ret; 624 808 }; 625 - imports.wbg.__wbindgen_cast_ba06f0048889102c = function(arg0, arg1) { 626 - // Cast intrinsic for `Closure(Closure { dtor_idx: 2202, function: Function { arguments: [Externref], shim_idx: 2203, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. 809 + imports.wbg.__wbindgen_cast_a206bfcc4c940d5e = function(arg0, arg1) { 810 + // Cast intrinsic for `Closure(Closure { dtor_idx: 2181, function: Function { arguments: [Externref], shim_idx: 2182, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. 627 811 const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen_1add006a0ed82fd3___closure__destroy___dyn_core_b125d98f3949a913___ops__function__FnMut__wasm_bindgen_1add006a0ed82fd3___JsValue____Output_______, wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___wasm_bindgen_1add006a0ed82fd3___JsValue_____); 628 812 return ret; 629 813 }; 630 - imports.wbg.__wbindgen_cast_dd8568660229206d = function(arg0, arg1) { 631 - // Cast intrinsic for `Closure(Closure { dtor_idx: 1434, function: Function { arguments: [], shim_idx: 1435, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. 814 + imports.wbg.__wbindgen_cast_e6dcebc0071508b8 = function(arg0, arg1) { 815 + // Cast intrinsic for `Closure(Closure { dtor_idx: 1096, function: Function { arguments: [], shim_idx: 1097, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. 816 + const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen_1add006a0ed82fd3___closure__destroy___dyn_core_b125d98f3949a913___ops__function__FnMut_____Output_______, wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke______); 817 + return ret; 818 + }; 819 + imports.wbg.__wbindgen_cast_ee2795d3f0e223fa = function(arg0, arg1) { 820 + // Cast intrinsic for `Closure(Closure { dtor_idx: 1359, function: Function { arguments: [], shim_idx: 1360, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. 632 821 const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen_1add006a0ed82fd3___closure__destroy___dyn_core_b125d98f3949a913___ops__function__FnMut_____Output________1_, wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke_______1_); 633 822 return ret; 634 823 };
+32 -1
crates/weaver-app/src/components/editor/component.rs
··· 93 93 let draft_key = draft_key.clone(); 94 94 let entry_uri = parsed_uri.clone(); 95 95 let initial_content = initial_content.clone(); 96 + let target_notebook = target_notebook.clone(); 96 97 97 98 async move { 99 + // Resolve target_notebook to a URI if provided 100 + let notebook_uri: Option<SmolStr> = if let Some(ref title) = target_notebook { 101 + if let Some(did) = fetcher.current_did().await { 102 + let ident = jacquard::types::ident::AtIdentifier::Did(did); 103 + match fetcher.get_notebook(ident, title.clone()).await { 104 + Ok(Some(notebook_data)) => { 105 + Some(notebook_data.0.uri.to_smolstr()) 106 + } 107 + Ok(None) | Err(_) => { 108 + tracing::debug!("Could not resolve notebook '{}' to URI", title); 109 + None 110 + } 111 + } 112 + } else { 113 + None 114 + } 115 + } else { 116 + None 117 + }; 118 + 98 119 match load_and_merge_document(&fetcher, &draft_key, entry_uri.as_ref()).await { 99 - Ok(Some(state)) => { 120 + Ok(Some(mut state)) => { 100 121 tracing::debug!("Loaded merged document state"); 122 + // If we resolved a notebook URI and state doesn't have one, use it 123 + if state.notebook_uri.is_none() { 124 + state.notebook_uri = notebook_uri; 125 + } 101 126 return LoadResult::Loaded(state); 102 127 } 103 128 Ok(None) => { ··· 302 327 synced_version: None, // Fresh from entry, never synced 303 328 last_seen_diffs: std::collections::HashMap::new(), 304 329 resolved_content, 330 + notebook_uri: notebook_uri.clone(), 305 331 }); 306 332 } 307 333 Err(e) => { ··· 327 353 synced_version: None, // New doc, never synced 328 354 last_seen_diffs: std::collections::HashMap::new(), 329 355 resolved_content: weaver_common::ResolvedContent::default(), 356 + notebook_uri, 330 357 }) 331 358 } 332 359 Err(e) => { ··· 732 759 cursor_offset, 733 760 editing_uri, 734 761 editing_cid, 762 + notebook_uri, 735 763 export_ms, 736 764 encode_ms, 737 765 } => { ··· 744 772 cursor_offset, 745 773 editing_uri, 746 774 editing_cid, 775 + notebook_uri, 747 776 }; 748 777 let write_start = crate::perf::now(); 749 778 let _ = gloo_storage::LocalStorage::set( ··· 848 877 let cursor_offset = doc.cursor.read().offset; 849 878 let editing_uri = doc.entry_ref().map(|r| r.uri.to_smolstr()); 850 879 let editing_cid = doc.entry_ref().map(|r| r.cid.to_smolstr()); 880 + let notebook_uri = doc.notebook_uri(); 851 881 852 882 let sink_clone = worker_sink.clone(); 853 883 ··· 865 895 cursor_offset, 866 896 editing_uri, 867 897 editing_cid, 898 + notebook_uri, 868 899 }) 869 900 .await; 870 901 }
+19
crates/weaver-app/src/components/editor/document.rs
··· 23 23 24 24 use jacquard::IntoStatic; 25 25 use jacquard::from_json_value; 26 + use jacquard::smol_str::SmolStr; 26 27 use jacquard::types::string::AtUri; 27 28 use weaver_api::com_atproto::repo::strong_ref::StrongRef; 28 29 use weaver_api::sh_weaver::embed::images::Image; ··· 90 91 /// None for new entries that haven't been published yet. 91 92 /// Signal so cloned docs share the same state after publish. 92 93 pub entry_ref: Signal<Option<StrongRef<'static>>>, 94 + 95 + /// AT-URI of the notebook this draft belongs to (for re-publishing) 96 + pub notebook_uri: Signal<Option<SmolStr>>, 93 97 94 98 // --- Edit sync state (for PDS sync) --- 95 99 /// StrongRef to the sh.weaver.edit.root record for this edit session. ··· 235 239 /// Pre-resolved embed content fetched during load. 236 240 /// Avoids embed pop-in on initial render. 237 241 pub resolved_content: weaver_common::ResolvedContent, 242 + /// Notebook URI for re-publishing to the same notebook. 243 + pub notebook_uri: Option<SmolStr>, 238 244 } 239 245 240 246 impl PartialEq for LoadedDocState { ··· 316 322 tags, 317 323 embeds, 318 324 entry_ref: Signal::new(None), 325 + notebook_uri: Signal::new(None), 319 326 edit_root: Signal::new(None), 320 327 last_diff: Signal::new(None), 321 328 last_synced_version: Signal::new(None), ··· 490 497 /// Set the StrongRef when editing an existing entry. 491 498 pub fn set_entry_ref(&mut self, entry: Option<StrongRef<'static>>) { 492 499 self.entry_ref.set(entry); 500 + } 501 + 502 + /// Get the notebook URI if this draft belongs to a notebook. 503 + pub fn notebook_uri(&self) -> Option<SmolStr> { 504 + self.notebook_uri.read().clone() 505 + } 506 + 507 + /// Set the notebook URI for re-publishing to the same notebook. 508 + pub fn set_notebook_uri(&mut self, uri: Option<SmolStr>) { 509 + self.notebook_uri.set(uri); 493 510 } 494 511 495 512 // --- Tags accessors --- ··· 1090 1107 tags, 1091 1108 embeds, 1092 1109 entry_ref: Signal::new(None), 1110 + notebook_uri: Signal::new(None), 1093 1111 edit_root: Signal::new(None), 1094 1112 last_diff: Signal::new(None), 1095 1113 last_synced_version: Signal::new(None), ··· 1148 1166 tags, 1149 1167 embeds, 1150 1168 entry_ref: Signal::new(state.entry_ref), 1169 + notebook_uri: Signal::new(state.notebook_uri), 1151 1170 edit_root: Signal::new(state.edit_root), 1152 1171 last_diff: Signal::new(state.last_diff), 1153 1172 // Use the synced version from state (tracks the PDS version vector)
+2 -2
crates/weaver-app/src/components/editor/mod.rs
··· 58 58 // Storage 59 59 #[allow(unused_imports)] 60 60 pub use storage::{ 61 - DRAFT_KEY_PREFIX, EditorSnapshot, clear_all_drafts, delete_draft, list_drafts, 62 - load_from_storage, load_snapshot_from_storage, save_to_storage, 61 + DRAFT_KEY_PREFIX, EditorSnapshot, clear_all_drafts, delete_draft, delete_draft_from_pds, 62 + list_drafts, load_from_storage, load_snapshot_from_storage, save_to_storage, 63 63 }; 64 64 65 65 // Sync
+42 -8
crates/weaver-app/src/components/editor/publish.rs
··· 4 4 5 5 use dioxus::prelude::*; 6 6 use jacquard::cowstr::ToCowStr; 7 + use jacquard::smol_str::ToSmolStr; 7 8 use jacquard::types::collection::Collection; 8 9 use jacquard::types::ident::AtIdentifier; 9 10 use jacquard::types::recordkey::RecordKey; ··· 252 253 .maybe_embeds(entry_embeds) 253 254 .build(); 254 255 256 + // Check if we have a stored notebook URI (for re-publishing to same notebook) 257 + // This avoids duplicate notebook creation when re-publishing 258 + let (notebook_uri, entry_refs) = if let Some(stored_uri) = doc.notebook_uri() { 259 + // Try to fetch notebook directly by URI to avoid duplicate creation 260 + match client.get_notebook_by_uri(&stored_uri).await { 261 + Ok(Some((uri, refs))) => { 262 + tracing::debug!("Found notebook by stored URI: {}", uri); 263 + (uri, refs) 264 + } 265 + Ok(None) | Err(_) => { 266 + // Stored URI invalid or notebook deleted, fall back to title lookup 267 + tracing::warn!("Stored notebook URI invalid, falling back to title lookup"); 268 + let (did, _) = client 269 + .session_info() 270 + .await 271 + .ok_or_else(|| WeaverError::InvalidNotebook("Not authenticated".into()))?; 272 + client.upsert_notebook(notebook, &did).await? 273 + } 274 + } 275 + } else { 276 + // No stored URI, use title-based lookup/creation 277 + let (did, _) = client 278 + .session_info() 279 + .await 280 + .ok_or_else(|| WeaverError::InvalidNotebook("Not authenticated".into()))?; 281 + client.upsert_notebook(notebook, &did).await? 282 + }; 283 + 255 284 // Pass existing rkey if re-publishing (to allow title changes without creating new entry) 256 285 let doc_entry_ref = doc.entry_ref(); 257 286 let existing_rkey = doc_entry_ref.as_ref().and_then(|r| r.uri.rkey()); 258 - let (entry_ref, was_created) = client 259 - .upsert_entry( 260 - notebook, 287 + 288 + // Use upsert_entry_with_notebook since we already have notebook data 289 + let (entry_ref, notebook_uri_final, was_created) = client 290 + .upsert_entry_with_notebook( 291 + notebook_uri, 292 + entry_refs, 261 293 &doc.title(), 262 294 entry, 263 295 existing_rkey.map(|r| r.0.as_str()), ··· 268 300 // Set entry_ref so subsequent publishes update this record 269 301 doc.set_entry_ref(Some(entry_ref)); 270 302 303 + // Store the notebook URI for future re-publishing 304 + doc.set_notebook_uri(Some(notebook_uri_final.to_smolstr())); 305 + 271 306 if was_created { 272 307 PublishResult::Created(uri) 273 308 } else { ··· 288 323 // Check if we're the owner or a collaborator 289 324 let owner_did = match existing_ref.uri.authority() { 290 325 AtIdentifier::Did(d) => d.clone(), 291 - AtIdentifier::Handle(h) => fetcher 292 - .client 293 - .resolve_handle(h) 294 - .await 295 - .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to resolve handle: {}", e)))?, 326 + AtIdentifier::Handle(h) => fetcher.client.resolve_handle(h).await.map_err(|e| { 327 + WeaverError::InvalidNotebook(format!("Failed to resolve handle: {}", e)) 328 + })?, 296 329 }; 297 330 let is_collaborator = owner_did != current_did; 298 331 ··· 304 337 .title(doc.title()) 305 338 .path(path) 306 339 .created_at(Datetime::now()) 340 + .updated_at(Datetime::now()) 307 341 .maybe_tags(tags) 308 342 .maybe_embeds(entry_embeds) 309 343 .build();
+80 -1
crates/weaver-app/src/components/editor/storage.rs
··· 22 22 use jacquard::IntoStatic; 23 23 use jacquard::smol_str::{SmolStr, ToSmolStr}; 24 24 use jacquard::types::string::{AtUri, Cid}; 25 - use weaver_api::com_atproto::repo::strong_ref::StrongRef; 26 25 use loro::cursor::Cursor; 27 26 use serde::{Deserialize, Serialize}; 27 + use weaver_api::com_atproto::repo::strong_ref::StrongRef; 28 28 29 29 use super::document::EditorDocument; 30 30 ··· 71 71 /// CID of the entry if editing an existing entry 72 72 #[serde(default, skip_serializing_if = "Option::is_none")] 73 73 pub editing_cid: Option<SmolStr>, 74 + 75 + /// AT-URI of the notebook this draft belongs to (for re-publishing) 76 + #[serde(default, skip_serializing_if = "Option::is_none")] 77 + pub notebook_uri: Option<SmolStr>, 74 78 } 75 79 76 80 /// Build the full storage key from a draft key. ··· 104 108 cursor_offset: doc.cursor.read().offset, 105 109 editing_uri: doc.entry_ref().map(|r| r.uri.to_smolstr()), 106 110 editing_cid: doc.entry_ref().map(|r| r.cid.to_smolstr()), 111 + notebook_uri: doc.notebook_uri(), 107 112 }; 108 113 109 114 let write_start = crate::perf::now(); ··· 151 156 // Verify the content matches (sanity check) 152 157 if doc.content() == snapshot.content { 153 158 doc.set_entry_ref(entry_ref.clone()); 159 + if let Some(notebook_uri) = snapshot.notebook_uri { 160 + doc.set_notebook_uri(Some(notebook_uri)); 161 + } 154 162 return Some(doc); 155 163 } 156 164 tracing::warn!("Snapshot content mismatch, falling back to text content"); ··· 162 170 doc.cursor.write().offset = snapshot.cursor_offset.min(doc.len_chars()); 163 171 doc.sync_loro_cursor(); 164 172 doc.set_entry_ref(entry_ref); 173 + if let Some(notebook_uri) = snapshot.notebook_uri { 174 + doc.set_notebook_uri(Some(notebook_uri)); 175 + } 165 176 Some(doc) 166 177 } 167 178 ··· 171 182 pub snapshot: Vec<u8>, 172 183 /// Entry StrongRef if editing an existing entry 173 184 pub entry_ref: Option<StrongRef<'static>>, 185 + /// Notebook URI for re-publishing 186 + pub notebook_uri: Option<SmolStr>, 174 187 } 175 188 176 189 /// Load snapshot data from LocalStorage (WASM only). ··· 201 214 Some(LocalSnapshotData { 202 215 snapshot: snapshot_bytes, 203 216 entry_ref, 217 + notebook_uri: snapshot.notebook_uri, 204 218 }) 205 219 } 206 220 ··· 248 262 } 249 263 250 264 drafts 265 + } 266 + 267 + /// Delete a draft stub record from PDS. 268 + /// 269 + /// This deletes the sh.weaver.edit.draft record, making the draft 270 + /// invisible in listDrafts. Edit history (edit.root, edit.diff) is 271 + /// preserved for potential recovery. 272 + #[cfg(all(target_family = "wasm", target_os = "unknown"))] 273 + pub async fn delete_draft_from_pds( 274 + fetcher: &crate::fetch::Fetcher, 275 + draft_key: &str, 276 + ) -> Result<(), weaver_common::WeaverError> { 277 + use jacquard::prelude::XrpcClient; 278 + use jacquard::types::ident::AtIdentifier; 279 + use jacquard::types::recordkey::RecordKey; 280 + use jacquard::types::string::Nsid; 281 + use weaver_api::com_atproto::repo::delete_record::DeleteRecord; 282 + 283 + // Only delete if authenticated 284 + let Some(did) = fetcher.current_did().await else { 285 + tracing::debug!("Not authenticated, skipping PDS draft deletion"); 286 + return Ok(()); 287 + }; 288 + 289 + // Extract rkey from draft_key 290 + let rkey_str = if let Some(tid) = draft_key.strip_prefix("new:") { 291 + tid.to_string() 292 + } else if draft_key.starts_with("at://") { 293 + draft_key.split('/').last().unwrap_or(draft_key).to_string() 294 + } else { 295 + draft_key.to_string() 296 + }; 297 + 298 + let rkey = RecordKey::any(&rkey_str) 299 + .map_err(|e| weaver_common::WeaverError::InvalidNotebook(e.to_string()))?; 300 + 301 + // Build the delete request 302 + let request = DeleteRecord::new() 303 + .repo(AtIdentifier::Did(did)) 304 + .collection(Nsid::new_static("sh.weaver.edit.draft").unwrap()) 305 + .rkey(rkey) 306 + .build(); 307 + 308 + // Execute deletion 309 + let client = fetcher.get_client(); 310 + match client.send(request).await { 311 + Ok(_) => { 312 + tracing::info!("Deleted draft stub from PDS: {}", draft_key); 313 + Ok(()) 314 + } 315 + Err(e) => { 316 + // Log but don't fail - draft may not exist on PDS 317 + tracing::warn!("Failed to delete draft from PDS (may not exist): {}", e); 318 + Ok(()) 319 + } 320 + } 321 + } 322 + 323 + /// Non-WASM stub for delete_draft_from_pds 324 + #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] 325 + pub async fn delete_draft_from_pds( 326 + _fetcher: &crate::fetch::Fetcher, 327 + _draft_key: &str, 328 + ) -> Result<(), weaver_common::WeaverError> { 329 + Ok(()) 251 330 } 252 331 253 332 /// Clear all editor drafts from LocalStorage (WASM only).
+242 -44
crates/weaver-app/src/components/editor/sync.rs
··· 239 239 } 240 240 } 241 241 242 + /// Convert a DocRef to an entry_ref StrongRef. 243 + /// 244 + /// For EntryRef: returns the entry's StrongRef directly 245 + /// For DraftRef: parses the draft_key as AT-URI, fetches the draft record to get CID, builds StrongRef 246 + /// For NotebookRef: returns the notebook's StrongRef 247 + async fn doc_ref_to_entry_ref( 248 + fetcher: &Fetcher, 249 + doc_ref: &DocRef<'_>, 250 + ) -> Option<StrongRef<'static>> { 251 + match &doc_ref.value { 252 + DocRefValue::EntryRef(entry_ref) => Some(entry_ref.entry.clone().into_static()), 253 + DocRefValue::DraftRef(draft_ref) => { 254 + // draft_key contains the canonical AT-URI: at://{did}/sh.weaver.edit.draft/{rkey} 255 + let draft_uri = AtUri::new(&draft_ref.draft_key).ok()?.into_static(); 256 + 257 + // Fetch the draft record to get its CID 258 + match fetcher.client.get_record::<Draft>(&draft_uri).await { 259 + Ok(response) => { 260 + let output = response.into_output().ok()?; 261 + let cid = output.cid?.into_static(); 262 + Some(StrongRef::new().uri(draft_uri).cid(cid).build()) 263 + } 264 + Err(e) => { 265 + tracing::warn!("Failed to fetch draft record for entry_ref: {}", e); 266 + None 267 + } 268 + } 269 + } 270 + DocRefValue::NotebookRef(notebook_ref) => Some(notebook_ref.notebook.clone().into_static()), 271 + DocRefValue::Unknown(_) => { 272 + tracing::warn!("Unknown DocRefValue variant, cannot convert to entry_ref"); 273 + None 274 + } 275 + } 276 + } 277 + 242 278 /// Result of a sync operation. 243 279 #[derive(Clone, Debug)] 244 280 pub enum SyncResult { ··· 629 665 Ok(all_diffs) 630 666 } 631 667 668 + /// Result of creating an edit root, includes optional draft stub info. 669 + pub struct CreateRootResult { 670 + /// The root record URI 671 + pub root_uri: AtUri<'static>, 672 + /// The root record CID 673 + pub root_cid: Cid<'static>, 674 + /// Draft stub StrongRef if this was a new draft (not editing published entry) 675 + pub draft_ref: Option<StrongRef<'static>>, 676 + } 677 + 632 678 /// Create the edit root record for an entry. 633 679 /// 634 680 /// Uploads the current Loro snapshot as a blob and creates an `sh.weaver.edit.root` 635 681 /// record referencing the entry (or draft key if unpublished). 636 682 /// 637 683 /// For drafts, also creates the `sh.weaver.edit.draft` stub record first. 684 + /// Returns the draft stub info so caller can set entry_ref. 638 685 pub async fn create_edit_root( 639 686 fetcher: &Fetcher, 640 687 doc: &EditorDocument, 641 688 draft_key: &str, 642 689 entry_uri: Option<&AtUri<'_>>, 643 690 entry_cid: Option<&Cid<'_>>, 644 - ) -> Result<(AtUri<'static>, Cid<'static>), WeaverError> { 691 + ) -> Result<CreateRootResult, WeaverError> { 645 692 let client = fetcher.get_client(); 646 693 let did = fetcher 647 694 .current_did() ··· 649 696 .ok_or_else(|| WeaverError::InvalidNotebook("Not authenticated".into()))?; 650 697 651 698 // For drafts, create the stub record first (makes it discoverable via listRecords) 652 - if entry_uri.is_none() { 699 + let draft_ref: Option<StrongRef<'static>> = if entry_uri.is_none() { 653 700 let rkey = extract_draft_rkey(draft_key); 654 - // Try to create draft stub, ignore if it already exists 701 + // Try to create draft stub, or get existing one 655 702 match create_draft_stub(fetcher, &did, &rkey).await { 656 - Ok((uri, _cid)) => { 703 + Ok((uri, cid)) => { 657 704 tracing::debug!("Created draft stub: {}", uri); 705 + Some(StrongRef::new().uri(uri).cid(cid).build()) 658 706 } 659 707 Err(e) => { 660 - // Check if it's a "record already exists" error - that's fine 708 + // Check if it's a "record already exists" error 661 709 let err_str = e.to_string(); 662 - if !err_str.contains("RecordAlreadyExists") && !err_str.contains("already exists") { 710 + if err_str.contains("RecordAlreadyExists") || err_str.contains("already exists") { 711 + // Draft exists - fetch it to get the CID 712 + let draft_uri_str = format!("at://{}/{}/{}", did, DRAFT_NSID, rkey); 713 + if let Ok(draft_uri) = AtUri::new(&draft_uri_str) { 714 + if let Ok(response) = 715 + fetcher.get_client().get_record::<Draft>(&draft_uri).await 716 + { 717 + if let Ok(output) = response.into_output() { 718 + if let Some(cid) = output.cid { 719 + Some( 720 + StrongRef::new() 721 + .uri(draft_uri.into_static()) 722 + .cid(cid.into_static()) 723 + .build(), 724 + ) 725 + } else { 726 + tracing::warn!("Draft exists but has no CID"); 727 + None 728 + } 729 + } else { 730 + tracing::warn!("Draft exists but couldn't parse output"); 731 + None 732 + } 733 + } else { 734 + tracing::warn!("Draft exists but couldn't fetch record"); 735 + None 736 + } 737 + } else { 738 + None 739 + } 740 + } else { 663 741 tracing::warn!("Failed to create draft stub (continuing anyway): {}", e); 742 + None 664 743 } 665 744 } 666 745 } 667 - } 746 + } else { 747 + None // Published entry, not a draft 748 + }; 668 749 669 750 // Export full snapshot 670 751 let snapshot = doc.export_snapshot(); ··· 708 789 .into_output() 709 790 .map_err(|e| WeaverError::InvalidNotebook(e.to_string()))?; 710 791 711 - Ok((output.uri.into_static(), output.cid.into_static())) 792 + Ok(CreateRootResult { 793 + root_uri: output.uri.into_static(), 794 + root_cid: output.cid.into_static(), 795 + draft_ref, 796 + }) 712 797 } 713 798 714 799 /// Create a diff record with updates since the last sync. ··· 827 912 if doc.edit_root().is_none() { 828 913 // First sync - create root 829 914 let create_start = crate::perf::now(); 830 - let (root_uri, root_cid) = create_edit_root( 915 + let result = create_edit_root( 831 916 fetcher, 832 917 doc, 833 918 draft_key, ··· 839 924 840 925 // Build StrongRef for the root 841 926 let root_ref = StrongRef::new() 842 - .uri(root_uri.clone()) 843 - .cid(root_cid.clone()) 927 + .uri(result.root_uri.clone()) 928 + .cid(result.root_cid.clone()) 844 929 .build(); 845 930 846 931 // Update document state ··· 848 933 doc.set_last_diff(None); 849 934 doc.mark_synced(); 850 935 936 + // For drafts: set entry_ref to the draft record (enables draft discovery/recovery) 937 + if let Some(draft_ref) = result.draft_ref { 938 + if doc.entry_ref().is_none() { 939 + tracing::debug!("Setting entry_ref to draft: {}", draft_ref.uri); 940 + doc.set_entry_ref(Some(draft_ref)); 941 + } 942 + } 943 + 851 944 let total_ms = crate::perf::now() - fn_start; 852 945 tracing::debug!(total_ms, create_ms, "sync_to_pds: created root"); 853 946 854 947 Ok(SyncResult::CreatedRoot { 855 - uri: root_uri, 856 - cid: root_cid, 948 + uri: result.root_uri, 949 + cid: result.root_cid, 857 950 }) 858 951 } else { 859 952 // Subsequent sync - create diff ··· 916 1009 /// Last seen diff URI per collaborator root (for incremental sync). 917 1010 /// Maps root URI -> last diff URI we've imported from that root. 918 1011 pub last_seen_diffs: std::collections::HashMap<AtUri<'static>, AtUri<'static>>, 1012 + /// The DocRef from the root record (tells us what's being edited) 1013 + pub doc_ref: DocRef<'static>, 919 1014 } 920 1015 921 1016 /// Fetch a blob from the PDS. ··· 1006 1101 let merged_doc = LoroDoc::new(); 1007 1102 let mut our_root_ref: Option<StrongRef<'static>> = None; 1008 1103 let mut our_last_diff_ref: Option<StrongRef<'static>> = None; 1104 + let mut merged_doc_ref: Option<DocRef<'static>> = None; 1009 1105 let mut updated_last_seen = last_seen_diffs.clone(); 1010 1106 1011 1107 // Get current user's DID to identify "our" root for sync state tracking ··· 1054 1150 updated_last_seen.insert(uri.clone(), last_diff.uri.clone().into_static()); 1055 1151 } 1056 1152 1153 + // Track doc_ref from the first root we process (they should all match) 1154 + if merged_doc_ref.is_none() { 1155 + merged_doc_ref = Some(pds_state.doc_ref.clone()); 1156 + } 1157 + 1057 1158 // Track "our" root/diff refs for sync state (used when syncing back) 1058 1159 // We want to track our own edit.root so subsequent diffs go to the right place 1059 1160 let is_our_root = current_did.as_ref().is_some_and(|did| root_did == *did); ··· 1089 1190 root_snapshot: merged_snapshot.into(), 1090 1191 diff_updates: vec![], // Already merged into snapshot 1091 1192 last_seen_diffs: updated_last_seen, 1193 + doc_ref: merged_doc_ref.expect("Should have at least one doc_ref if we have a root"), 1092 1194 })) 1093 1195 } 1094 1196 ··· 1130 1232 .uri(root_uri.clone()) 1131 1233 .cid(root_cid.into_static()) 1132 1234 .build(); 1235 + 1236 + // Extract the DocRef from the root record 1237 + let doc_ref = root_output.value.doc.into_static(); 1133 1238 1134 1239 // Fetch the root snapshot blob 1135 1240 let root_snapshot = fetch_blob( ··· 1149 1254 root_snapshot, 1150 1255 diff_updates: vec![], 1151 1256 last_seen_diffs: std::collections::HashMap::new(), 1257 + doc_ref, 1152 1258 })); 1153 1259 } 1154 1260 ··· 1170 1276 } 1171 1277 } 1172 1278 1173 - let diff_uri = AtUri::new(&format_smolstr!("at://{}/{}/{}", diff_id.did, DIFF_NSID, rkey_str)) 1174 - .map_err(|e| WeaverError::InvalidNotebook(format!("Invalid diff URI: {}", e)))? 1175 - .into_static(); 1279 + let diff_uri = AtUri::new(&format_smolstr!( 1280 + "at://{}/{}/{}", 1281 + diff_id.did, 1282 + DIFF_NSID, 1283 + rkey_str 1284 + )) 1285 + .map_err(|e| WeaverError::InvalidNotebook(format!("Invalid diff URI: {}", e)))? 1286 + .into_static(); 1176 1287 1177 1288 let diff_response = fetcher 1178 1289 .client ··· 1226 1337 root_snapshot, 1227 1338 diff_updates, 1228 1339 last_seen_diffs: std::collections::HashMap::new(), 1340 + doc_ref, 1229 1341 })) 1230 1342 } 1231 1343 ··· 1291 1403 synced_version: None, // Local-only, never synced to PDS 1292 1404 last_seen_diffs: std::collections::HashMap::new(), 1293 1405 resolved_content, 1406 + notebook_uri: local.notebook_uri, // Restored from localStorage 1294 1407 })) 1295 1408 } 1296 1409 ··· 1314 1427 // Capture the version after loading all PDS state - this is our sync baseline 1315 1428 let synced_version = Some(doc.oplog_vv()); 1316 1429 1430 + // Reconstruct entry_ref from the DocRef stored in edit.root 1431 + let entry_ref = doc_ref_to_entry_ref(fetcher, &pds.doc_ref).await; 1432 + if entry_ref.is_some() { 1433 + tracing::debug!("Reconstructed entry_ref from PDS DocRef"); 1434 + } 1435 + 1317 1436 let resolved_content = 1318 1437 prefetch_embeds_from_doc(&doc, fetcher, owner_ident.as_deref()).await; 1319 1438 1320 1439 Ok(Some(LoadedDocState { 1321 1440 doc, 1322 - entry_ref: None, // Entry ref comes from the entry itself, not edit state 1441 + entry_ref, 1323 1442 edit_root: Some(pds.root_ref), 1324 1443 last_diff: pds.last_diff_ref, 1325 1444 synced_version, // Just loaded from PDS, fully synced 1326 1445 last_seen_diffs: pds.last_seen_diffs, 1327 1446 resolved_content, 1447 + notebook_uri: None, // PDS-only, notebook context comes from target_notebook 1328 1448 })) 1329 1449 } 1330 1450 ··· 1377 1497 synced_version: Some(pds_version), 1378 1498 last_seen_diffs: pds.last_seen_diffs, 1379 1499 resolved_content, 1500 + notebook_uri: local.notebook_uri, // Restored from localStorage 1380 1501 })) 1381 1502 } 1382 1503 } ··· 1413 1534 pub document: EditorDocument, 1414 1535 /// Draft key for this document 1415 1536 pub draft_key: String, 1416 - /// Auto-sync interval in milliseconds (0 to disable) 1417 - #[props(default = 30_000)] 1537 + /// Auto-sync interval in milliseconds (0 to disable, default disabled) 1538 + #[props(default = 0)] 1418 1539 pub auto_sync_interval_ms: u32, 1419 1540 /// Callback to refresh/reload document from collaborators 1420 1541 #[props(default)] ··· 1427 1548 /// Sync status indicator with auto-sync functionality. 1428 1549 /// 1429 1550 /// Displays the current sync state and automatically syncs to PDS periodically. 1551 + /// Initially shows "Start syncing" until user activates sync, then auto-syncs. 1430 1552 #[component] 1431 1553 pub fn SyncStatus(props: SyncStatusProps) -> Element { 1432 1554 let fetcher = use_context::<Fetcher>(); 1433 1555 let auth_state = use_context::<Signal<AuthState>>(); 1434 1556 1557 + let doc = props.document.clone(); 1558 + let draft_key = props.draft_key.clone(); 1559 + 1560 + // Sync activated - true if sync has been started (either manually or doc already has edit_root) 1561 + // Once activated, auto-sync is enabled 1562 + let mut sync_activated = use_signal(|| { 1563 + // If document already has an edit_root, syncing is already active 1564 + props.document.edit_root().is_some() 1565 + }); 1566 + 1435 1567 // Sync state management 1436 1568 let mut sync_state = use_signal(|| { 1437 1569 if props.document.has_unsynced_changes() { ··· 1442 1574 }); 1443 1575 let mut last_error: Signal<Option<String>> = use_signal(|| None); 1444 1576 1445 - let doc = props.document.clone(); 1446 - let draft_key = props.draft_key.clone(); 1447 - 1448 1577 // Check if we're authenticated (drafts can sync via DraftRef even without entry) 1449 1578 let is_authenticated = auth_state.read().is_authenticated(); 1450 1579 1451 1580 // Auto-sync trigger signal - set to true to trigger a sync 1452 1581 let mut trigger_sync = use_signal(|| false); 1453 1582 1454 - // Auto-sync timer - triggers sync when there are unsynced changes 1583 + // Auto-sync timer - only triggers after sync has been activated 1455 1584 { 1456 - let auto_sync_interval_ms = props.auto_sync_interval_ms; 1457 1585 let doc_for_check = doc.clone(); 1458 1586 1459 - dioxus_sdk::time::use_interval( 1460 - std::time::Duration::from_millis(auto_sync_interval_ms as u64), 1461 - move |_| { 1462 - if auto_sync_interval_ms == 0 { 1463 - return; 1464 - } 1465 - // Only trigger if there are unsynced changes 1466 - if doc_for_check.has_unsynced_changes() { 1467 - trigger_sync.set(true); 1468 - } 1469 - }, 1470 - ); 1587 + // Use 30s interval for auto-sync once activated 1588 + dioxus_sdk::time::use_interval(std::time::Duration::from_secs(30), move |_| { 1589 + // Only auto-sync if activated 1590 + if !*sync_activated.peek() { 1591 + return; 1592 + } 1593 + // Only trigger if there are unsynced changes 1594 + if doc_for_check.has_unsynced_changes() { 1595 + trigger_sync.set(true); 1596 + } 1597 + }); 1471 1598 } 1472 1599 1473 1600 // Collaborator poll timer - checks for collaborator updates periodically ··· 1572 1699 Ok(_) => { 1573 1700 sync_state.set(SyncState::Synced); 1574 1701 last_error.set(None); 1702 + // Activate auto-sync after first successful sync 1703 + if !*sync_activated.peek() { 1704 + sync_activated.set(true); 1705 + tracing::debug!("Sync activated - auto-sync enabled"); 1706 + } 1575 1707 tracing::debug!("Sync completed successfully"); 1576 1708 } 1577 1709 Err(e) => { ··· 1584 1716 }); 1585 1717 1586 1718 // Determine display state (drafts can sync too via DraftRef) 1719 + let is_activated = *sync_activated.read(); 1587 1720 let display_state = if !is_authenticated { 1588 1721 SyncState::Disabled 1589 1722 } else { 1590 1723 *sync_state.read() 1591 1724 }; 1592 1725 1593 - let (icon, label, class) = match display_state { 1594 - SyncState::Synced => ("✓", "Synced", "sync-status synced"), 1595 - SyncState::Syncing => ("◌", "Syncing...", "sync-status syncing"), 1596 - SyncState::Unsynced => ("●", "Unsynced", "sync-status unsynced"), 1597 - SyncState::RemoteChanges => ("↓", "Updates", "sync-status remote-changes"), 1598 - SyncState::Error => ("✕", "Sync error", "sync-status error"), 1599 - SyncState::Disabled => ("○", "Sync disabled", "sync-status disabled"), 1726 + // Before activation: show "Start syncing" button 1727 + // After activation: show normal sync states 1728 + let (icon, label, class) = if !is_activated && is_authenticated { 1729 + ("▶", "Start syncing", "sync-status start-sync") 1730 + } else { 1731 + match display_state { 1732 + SyncState::Synced => ("✓", "Synced", "sync-status synced"), 1733 + SyncState::Syncing => ("◌", "Syncing...", "sync-status syncing"), 1734 + SyncState::Unsynced => ("●", "Unsynced", "sync-status unsynced"), 1735 + SyncState::RemoteChanges => ("↓", "Updates", "sync-status remote-changes"), 1736 + SyncState::Error => ("✕", "Sync error", "sync-status error"), 1737 + SyncState::Disabled => ("○", "Sync disabled", "sync-status disabled"), 1738 + } 1739 + }; 1740 + 1741 + // Long-press detection for deactivating sync 1742 + let mut long_press_active = use_signal(|| false); 1743 + #[cfg(target_arch = "wasm32")] 1744 + let mut long_press_timeout: Signal<Option<gloo_timers::callback::Timeout>> = 1745 + use_signal(|| None); 1746 + 1747 + let on_pointer_down = move |_: dioxus::events::PointerEvent| { 1748 + // Only allow deactivation if sync is currently activated 1749 + if !*sync_activated.peek() { 1750 + return; 1751 + } 1752 + 1753 + long_press_active.set(true); 1754 + 1755 + // Start 1 second timer for long press 1756 + #[cfg(target_arch = "wasm32")] 1757 + let timeout = gloo_timers::callback::Timeout::new(1000, move || { 1758 + if *long_press_active.peek() { 1759 + sync_activated.set(false); 1760 + long_press_active.set(false); 1761 + tracing::debug!("Sync deactivated via long press"); 1762 + } 1763 + }); 1764 + #[cfg(target_arch = "wasm32")] 1765 + long_press_timeout.set(Some(timeout)); 1766 + }; 1767 + 1768 + let on_pointer_up = move |_: dioxus::events::PointerEvent| { 1769 + long_press_active.set(false); 1770 + // Cancel the timeout by dropping it 1771 + #[cfg(target_arch = "wasm32")] 1772 + long_press_timeout.set(None); 1773 + }; 1774 + 1775 + let on_pointer_leave = move |_: dioxus::events::PointerEvent| { 1776 + long_press_active.set(false); 1777 + #[cfg(target_arch = "wasm32")] 1778 + long_press_timeout.set(None); 1600 1779 }; 1601 1780 1602 1781 // Combined sync handler - pulls remote changes first if needed, then pushes local ··· 1605 1784 let on_refresh = props.on_refresh.clone(); 1606 1785 let current_state = display_state; 1607 1786 move |_: dioxus::events::MouseEvent| { 1787 + // Don't trigger click if long press just fired 1788 + if !*sync_activated.peek() && *long_press_active.peek() { 1789 + return; 1790 + } 1791 + 1608 1792 if *sync_state.peek() == SyncState::Syncing { 1609 1793 return; // Already syncing 1610 1794 } ··· 1623 1807 } 1624 1808 }; 1625 1809 1810 + // Show tooltip hint about long-press when sync is active 1811 + let title = if is_activated { 1812 + if let Some(ref err) = *last_error.read() { 1813 + err.clone() 1814 + } else { 1815 + format!("{} (hold to stop syncing)", label) 1816 + } 1817 + } else { 1818 + label.to_string() 1819 + }; 1820 + 1626 1821 rsx! { 1627 1822 div { 1628 1823 class: "{class}", 1629 - title: if let Some(ref err) = *last_error.read() { err.clone() } else { label.to_string() }, 1824 + title: "{title}", 1630 1825 onclick: on_sync_click, 1826 + onpointerdown: on_pointer_down, 1827 + onpointerup: on_pointer_up, 1828 + onpointerleave: on_pointer_leave, 1631 1829 1632 1830 span { class: "sync-icon", "{icon}" } 1633 1831 span { class: "sync-label", "{label}" }
+8
crates/weaver-app/src/components/editor/worker.rs
··· 43 43 editing_uri: Option<SmolStr>, 44 44 /// Editing CID if editing existing entry 45 45 editing_cid: Option<SmolStr>, 46 + /// Notebook URI for re-publishing 47 + notebook_uri: Option<SmolStr>, 46 48 }, 47 49 /// Start collab session (worker will spawn CollabNode) 48 50 StartCollab { ··· 100 102 editing_uri: Option<SmolStr>, 101 103 /// Editing CID 102 104 editing_cid: Option<SmolStr>, 105 + /// Notebook URI for re-publishing 106 + notebook_uri: Option<SmolStr>, 103 107 /// Export timing in ms 104 108 export_ms: f64, 105 109 /// Encode timing in ms ··· 266 270 cursor_offset, 267 271 editing_uri, 268 272 editing_cid, 273 + notebook_uri, 269 274 } => { 270 275 let Some(ref doc) = doc else { 271 276 if let Err(e) = scope ··· 314 319 cursor_offset, 315 320 editing_uri, 316 321 editing_cid, 322 + notebook_uri, 317 323 export_ms, 318 324 encode_ms, 319 325 }) ··· 663 669 cursor_offset, 664 670 editing_uri, 665 671 editing_cid, 672 + notebook_uri, 666 673 } => { 667 674 let Some(ref doc) = doc else { 668 675 if let Err(e) = scope ··· 707 714 cursor_offset, 708 715 editing_uri, 709 716 editing_cid, 717 + notebook_uri, 710 718 export_ms, 711 719 encode_ms, 712 720 })
+1 -2
crates/weaver-app/src/components/entry.rs
··· 605 605 606 606 div { class: "entry-meta-info", 607 607 // Authors 608 - if !entry_view.authors.is_empty() { 609 608 div { class: "entry-authors", 610 609 AuthorList { 611 610 authors: entry_view.authors.clone(), 612 611 owner_ident: Some(ident.clone()), 613 612 } 614 613 } 615 - } 614 + 616 615 617 616 // Date 618 617 div { class: "entry-date",
+17 -2
crates/weaver-app/src/views/drafts.rs
··· 5 5 use crate::components::button::{Button, ButtonVariant}; 6 6 use crate::components::dialog::{DialogContent, DialogDescription, DialogRoot, DialogTitle}; 7 7 use crate::components::editor::{list_drafts_from_pds, RemoteDraft}; 8 - use crate::components::editor::{delete_draft, list_drafts}; 8 + use crate::components::editor::{delete_draft, delete_draft_from_pds, list_drafts}; 9 9 use crate::fetch::Fetcher; 10 10 use dioxus::prelude::*; 11 11 use jacquard::smol_str::{SmolStr, format_smolstr}; ··· 39 39 let mut local_drafts = use_signal(list_drafts); 40 40 let mut show_delete_confirm = use_signal(|| None::<String>); 41 41 42 + // Clone fetcher early for use in both resource and delete handler 43 + let fetcher_for_resource = fetcher.clone(); 44 + let fetcher_for_delete = fetcher.clone(); 45 + 42 46 // Fetch remote drafts from PDS (depends on auth state to re-run when logged in) 43 47 let remote_drafts_resource = use_resource(move || { 44 - let fetcher = fetcher.clone(); 48 + let fetcher = fetcher_for_resource.clone(); 45 49 let _did = auth_state.read().did.clone(); // Track auth state for reactivity 46 50 async move { list_drafts_from_pds(&fetcher).await.ok().unwrap_or_default() } 47 51 }); ··· 131 135 }); 132 136 133 137 let mut handle_delete = move |key: String| { 138 + let fetcher = fetcher_for_delete.clone(); 139 + let key_clone = key.clone(); 140 + 141 + // Delete from localStorage immediately 134 142 delete_draft(&key); 135 143 local_drafts.set(list_drafts()); 136 144 show_delete_confirm.set(None); 145 + 146 + // Also delete from PDS (async, fire-and-forget) 147 + spawn(async move { 148 + if let Err(e) = delete_draft_from_pds(&fetcher, &key_clone).await { 149 + tracing::warn!("Failed to delete draft from PDS: {}", e); 150 + } 151 + }); 137 152 }; 138 153 139 154 rsx! {
+6 -1
crates/weaver-app/src/views/home.rs
··· 10 10 /// Pinned content items - can be notebooks or entries 11 11 #[derive(Clone, PartialEq)] 12 12 pub enum PinnedItem { 13 + #[allow(dead_code)] 13 14 Notebook { 14 15 ident: AtIdentifier<'static>, 15 16 title: SmolStr, ··· 31 32 // }, 32 33 PinnedItem::Entry { 33 34 ident: AtIdentifier::Did(Did::new_static("did:plc:yfvwmnlztr4dwkb7hwz55r2g").unwrap()), 34 - rkey: SmolStr::new_static("3m4rbphjzt62b"), 35 + rkey: SmolStr::new_static("3m7ysqf2z5s22"), 35 36 }, 37 + // PinnedItem::Entry { 38 + // ident: AtIdentifier::Did(Did::new_static("did:plc:yfvwmnlztr4dwkb7hwz55r2g").unwrap()), 39 + // rkey: SmolStr::new_static("3m4rbphjzt62b"), 40 + // }, 36 41 ] 37 42 } 38 43
+1 -1
crates/weaver-cli/src/main.rs
··· 388 388 // Use WeaverExt to upsert entry (handles notebook + entry creation/updates) 389 389 use jacquard::http_client::HttpClient; 390 390 use weaver_common::WeaverExt; 391 - let (entry_ref, was_created) = agent 391 + let (entry_ref, _, was_created) = agent 392 392 .upsert_entry(&title, entry_title.as_ref(), entry, None) 393 393 .await?; 394 394
+97 -27
crates/weaver-common/src/agent.rs
··· 123 123 } 124 124 } 125 125 126 + /// Fetch a notebook by URI and return its entry list 127 + /// 128 + /// Returns Ok(Some((uri, entry_list))) if the notebook exists and can be parsed, 129 + /// Ok(None) if the notebook doesn't exist, 130 + /// Err if there's a network or parsing error. 131 + fn get_notebook_by_uri( 132 + &self, 133 + uri: &str, 134 + ) -> impl Future<Output = Result<Option<(AtUri<'static>, Vec<StrongRef<'static>>)>, WeaverError>> 135 + where 136 + Self: Sized, 137 + { 138 + async move { 139 + use weaver_api::sh_weaver::notebook::book::Book; 140 + 141 + let at_uri = AtUri::new(uri) 142 + .map_err(|e| WeaverError::InvalidNotebook(format!("Invalid notebook URI: {}", e)))?; 143 + 144 + let response = match self.get_record::<Book>(&at_uri).await { 145 + Ok(r) => r, 146 + Err(_) => return Ok(None), // Notebook doesn't exist 147 + }; 148 + 149 + let output = match response.into_output() { 150 + Ok(o) => o, 151 + Err(_) => return Ok(None), // Failed to parse 152 + }; 153 + 154 + let entries = output 155 + .value 156 + .entry_list 157 + .iter() 158 + .cloned() 159 + .map(IntoStatic::into_static) 160 + .collect(); 161 + 162 + Ok(Some((at_uri.into_static(), entries))) 163 + } 164 + } 165 + 126 166 /// Find or create a notebook by title, returning its URI and entry list 127 167 /// 128 168 /// If the notebook doesn't exist, creates it with the given DID as author. ··· 208 248 } 209 249 } 210 250 211 - /// Find or create an entry within a notebook 251 + /// Find or create an entry within a notebook (with pre-fetched notebook data) 212 252 /// 213 - /// Multi-step workflow: 214 - /// 1. Find the notebook by title 215 - /// 2. If existing_rkey is provided, match by rkey; otherwise match by title 216 - /// 3. If found: update the entry with new content 217 - /// 4. If not found: create new entry and append to notebook's entry_list 218 - /// 219 - /// The `existing_rkey` parameter allows updating an entry even if its title changed, 220 - /// and enables pre-generating rkeys for path rewriting before publish. 253 + /// This variant accepts notebook URI and entry_refs directly to avoid redundant 254 + /// notebook lookups when the caller has already fetched this data. 221 255 /// 222 - /// Returns (entry_ref, was_created) 223 - fn upsert_entry( 256 + /// Returns (entry_ref, notebook_uri, was_created) 257 + fn upsert_entry_with_notebook( 224 258 &self, 225 - notebook_title: &str, 259 + notebook_uri: AtUri<'static>, 260 + entry_refs: Vec<StrongRef<'static>>, 226 261 entry_title: &str, 227 262 entry: entry::Entry<'_>, 228 263 existing_rkey: Option<&str>, 229 - ) -> impl Future<Output = Result<(StrongRef<'static>, bool), WeaverError>> 264 + ) -> impl Future<Output = Result<(StrongRef<'static>, AtUri<'static>, bool), WeaverError>> 230 265 where 231 266 Self: Sized, 232 267 { 233 268 async move { 234 - // Get our own DID 235 - let (did, _) = self.session_info().await.ok_or_else(|| { 236 - AgentError::from(ClientError::invalid_request("No session info available")) 237 - })?; 238 - 239 - // Find or create notebook 240 - let (notebook_uri, entry_refs) = self.upsert_notebook(notebook_title, &did).await?; 241 - 242 269 // If we have an existing rkey, try to find and update that specific entry 243 270 if let Some(rkey) = existing_rkey { 244 271 // Check if this entry exists in the notebook by comparing rkeys ··· 259 286 .uri(output.uri.into_static()) 260 287 .cid(output.cid.into_static()) 261 288 .build(); 262 - return Ok((updated_ref, false)); 289 + return Ok((updated_ref, notebook_uri, false)); 263 290 } 264 291 } 265 292 ··· 283 310 }) 284 311 .await?; 285 312 286 - return Ok((new_ref, true)); 313 + return Ok((new_ref, notebook_uri, true)); 287 314 } 288 315 289 - // No existing rkey - use title-based matching (original behavior) 316 + // No existing rkey - use title-based matching 290 317 291 318 // Fast path: if notebook is empty, skip search and create directly 292 319 if entry_refs.is_empty() { ··· 307 334 }) 308 335 .await?; 309 336 310 - return Ok((new_ref, true)); 337 + return Ok((new_ref, notebook_uri, true)); 311 338 } 312 339 313 340 // Check if entry with this title exists in the notebook ··· 331 358 .uri(output.uri.into_static()) 332 359 .cid(output.cid.into_static()) 333 360 .build(); 334 - return Ok((updated_ref, false)); 361 + return Ok((updated_ref, notebook_uri, false)); 335 362 } 336 363 } 337 364 } ··· 355 382 }) 356 383 .await?; 357 384 358 - Ok((new_ref, true)) 385 + Ok((new_ref, notebook_uri, true)) 386 + } 387 + } 388 + 389 + /// Find or create an entry within a notebook 390 + /// 391 + /// Multi-step workflow: 392 + /// 1. Find the notebook by title 393 + /// 2. If existing_rkey is provided, match by rkey; otherwise match by title 394 + /// 3. If found: update the entry with new content 395 + /// 4. If not found: create new entry and append to notebook's entry_list 396 + /// 397 + /// The `existing_rkey` parameter allows updating an entry even if its title changed, 398 + /// and enables pre-generating rkeys for path rewriting before publish. 399 + /// 400 + /// Returns (entry_ref, notebook_uri, was_created) 401 + fn upsert_entry( 402 + &self, 403 + notebook_title: &str, 404 + entry_title: &str, 405 + entry: entry::Entry<'_>, 406 + existing_rkey: Option<&str>, 407 + ) -> impl Future<Output = Result<(StrongRef<'static>, AtUri<'static>, bool), WeaverError>> 408 + where 409 + Self: Sized, 410 + { 411 + async move { 412 + // Get our own DID 413 + let (did, _) = self.session_info().await.ok_or_else(|| { 414 + AgentError::from(ClientError::invalid_request("No session info available")) 415 + })?; 416 + 417 + // Find or create notebook 418 + let (notebook_uri, entry_refs) = self.upsert_notebook(notebook_title, &did).await?; 419 + 420 + // Delegate to the variant with pre-fetched notebook data 421 + self.upsert_entry_with_notebook( 422 + notebook_uri, 423 + entry_refs, 424 + entry_title, 425 + entry, 426 + existing_rkey, 427 + ) 428 + .await 359 429 } 360 430 } 361 431
+4
crates/weaver-index/Cargo.toml
··· 64 64 chrono = { version = "0.4", features = ["serde"] } 65 65 smol_str = "0.3" 66 66 67 + # CRDT (for parsing draft snapshots) 68 + loro = { workspace = true } 69 + 67 70 # CID handling (for CAR block lookups) 68 71 cid = "0.11" 69 72 ··· 73 76 dashmap = "6" 74 77 include_dir = "0.7.4" 75 78 regex = "1" 79 + mini-moka = { git = "https://github.com/moka-rs/mini-moka", rev = "da864e849f5d034f32e02197fee9bb5d5af36d3d" } 76 80 77 81 # WebSocket (for tap consumer) 78 82 tokio-tungstenite = { version = "0.26", features = ["native-tls"] }
+22
crates/weaver-index/migrations/clickhouse/040_draft_titles.sql
··· 1 + -- Draft titles extracted from Loro snapshots 2 + -- Updated by background task when edit_heads changes 3 + 4 + CREATE TABLE IF NOT EXISTS draft_titles ( 5 + -- Draft identity (matches drafts table) 6 + did String, 7 + rkey String, 8 + 9 + -- Extracted title from Loro doc 10 + title String DEFAULT '', 11 + 12 + -- Head used for extraction (stale if doesn't match edit_heads) 13 + head_did String DEFAULT '', 14 + head_rkey String DEFAULT '', 15 + head_cid String DEFAULT '', 16 + 17 + -- Timestamps 18 + updated_at DateTime64(3) DEFAULT now64(3), 19 + indexed_at DateTime64(3) DEFAULT now64(3) 20 + ) 21 + ENGINE = ReplacingMergeTree(indexed_at) 22 + ORDER BY (did, rkey)
+15 -2
crates/weaver-index/src/bin/weaver_indexer.rs
··· 2 2 3 3 use clap::{Parser, Subcommand}; 4 4 use tracing::{error, info, warn}; 5 + use jacquard::client::UnauthenticatedSession; 5 6 use weaver_index::clickhouse::InserterConfig; 6 7 use weaver_index::clickhouse::{Client, Migrator}; 7 8 use weaver_index::config::{ ··· 9 10 }; 10 11 use weaver_index::firehose::FirehoseConsumer; 11 12 use weaver_index::server::{AppState, ServerConfig, TelemetryConfig, telemetry}; 12 - use weaver_index::{FirehoseIndexer, ServiceIdentity, TapIndexer, load_cursor}; 13 + use weaver_index::{ 14 + DraftTitleTaskConfig, FirehoseIndexer, ServiceIdentity, TapIndexer, load_cursor, 15 + run_draft_title_task, 16 + }; 13 17 14 18 #[derive(Parser)] 15 19 #[command(name = "indexer")] ··· 165 169 }); 166 170 let did_doc = identity.did_document_with_service(&server_config.service_did, &service_endpoint); 167 171 168 - // Create separate clients for indexer and server 172 + // Create separate clients for indexer, server, and background tasks 169 173 let indexer_client = Client::new(&ch_config)?; 170 174 let server_client = Client::new(&ch_config)?; 175 + let task_client = std::sync::Arc::new(Client::new(&ch_config)?); 171 176 172 177 // Build AppState for server 173 178 let state = AppState::new( ··· 208 213 tokio::spawn(async move { indexer.run().await }) 209 214 } 210 215 }; 216 + 217 + // Spawn background tasks 218 + let resolver = UnauthenticatedSession::new_public(); 219 + tokio::spawn(run_draft_title_task( 220 + task_client, 221 + resolver, 222 + DraftTitleTaskConfig::default(), 223 + )); 211 224 212 225 // Run server, monitoring indexer health 213 226 tokio::select! {
+2 -2
crates/weaver-index/src/clickhouse.rs
··· 7 7 pub use client::{Client, TableSize}; 8 8 pub use migrations::{DbObject, MigrationResult, Migrator, ObjectType}; 9 9 pub use queries::{ 10 - CollaboratorRow, EditHeadRow, EditNodeRow, EntryRow, HandleMappingRow, NotebookRow, 11 - ProfileCountsRow, ProfileRow, ProfileWithCounts, 10 + CollaboratorRow, EditChainNode, EditHeadRow, EditNodeRow, EntryRow, HandleMappingRow, 11 + NotebookRow, ProfileCountsRow, ProfileRow, ProfileWithCounts, StaleDraftRow, 12 12 }; 13 13 pub use resilient_inserter::{InserterConfig, ResilientRecordInserter}; 14 14 pub use schema::{
+1 -1
crates/weaver-index/src/clickhouse/queries.rs
··· 12 12 13 13 pub use collab::PermissionRow; 14 14 pub use collab_state::{CollaboratorRow, EditHeadRow}; 15 - pub use edit::EditNodeRow; 15 + pub use edit::{EditChainNode, EditNodeRow, StaleDraftRow}; 16 16 pub use identity::HandleMappingRow; 17 17 pub use notebooks::{EntryRow, NotebookRow}; 18 18 pub use profiles::{ProfileCountsRow, ProfileRow, ProfileWithCounts};
+188 -3
crates/weaver-index/src/clickhouse/queries/edit.rs
··· 40 40 pub root_cid: SmolStr, 41 41 #[serde(with = "clickhouse::serde::chrono::datetime64::millis::option")] 42 42 pub last_edit_at: Option<chrono::DateTime<chrono::Utc>>, 43 + /// Title extracted from Loro doc (may be empty if not yet extracted) 44 + pub title: SmolStr, 45 + } 46 + 47 + /// Draft needing title extraction (stale or missing from draft_titles) 48 + #[derive(Debug, Clone, Row, Deserialize)] 49 + pub struct StaleDraftRow { 50 + /// Draft DID 51 + pub did: SmolStr, 52 + /// Draft rkey 53 + pub rkey: SmolStr, 54 + /// Current head DID 55 + pub head_did: SmolStr, 56 + /// Current head rkey 57 + pub head_rkey: SmolStr, 58 + /// Current head CID 59 + pub head_cid: SmolStr, 60 + /// Root DID for this edit chain 61 + pub root_did: SmolStr, 62 + /// Root rkey 63 + pub root_rkey: SmolStr, 64 + /// Root CID 65 + pub root_cid: SmolStr, 66 + } 67 + 68 + /// Edit chain node for reconstructing Loro doc 69 + #[derive(Debug, Clone, Row, Deserialize)] 70 + pub struct EditChainNode { 71 + pub did: SmolStr, 72 + pub rkey: SmolStr, 73 + pub cid: SmolStr, 74 + pub node_type: SmolStr, 75 + pub prev_did: SmolStr, 76 + pub prev_rkey: SmolStr, 43 77 } 44 78 45 79 impl Client { ··· 108 142 109 143 /// List drafts for an actor. 110 144 /// 111 - /// Returns draft records with associated edit root info if available. 145 + /// Returns draft records with associated edit root info and title if available. 112 146 pub async fn list_drafts( 113 147 &self, 114 148 actor_did: &str, 115 149 cursor: Option<i64>, 116 150 limit: i64, 117 151 ) -> Result<Vec<DraftWithRootRow>, IndexError> { 118 - // Query drafts table with LEFT JOIN to get associated edit roots 152 + // Query drafts table with LEFT JOINs to get edit roots and titles 119 153 // Edit roots reference drafts via resource_type/did/rkey fields 154 + // Titles are extracted from Loro snapshots by background task 120 155 let query = r#" 121 156 SELECT 122 157 d.did, ··· 126 161 COALESCE(e.did, '') AS root_did, 127 162 COALESCE(e.rkey, '') AS root_rkey, 128 163 COALESCE(e.cid, '') AS root_cid, 129 - e.created_at AS last_edit_at 164 + e.created_at AS last_edit_at, 165 + COALESCE(t.title, '') AS title 130 166 FROM drafts d FINAL 131 167 LEFT JOIN ( 132 168 SELECT ··· 141 177 AND resource_type = 'draft' 142 178 AND deleted_at = toDateTime64(0, 3) 143 179 ) e ON e.resource_did = d.did AND e.resource_rkey = d.rkey 180 + LEFT JOIN draft_titles t FINAL ON t.did = d.did AND t.rkey = d.rkey 144 181 WHERE d.did = ? 145 182 AND d.deleted_at = toDateTime64(0, 3) 146 183 AND (? = 0 OR toUnixTimestamp64Milli(d.created_at) < ?) ··· 165 202 })?; 166 203 167 204 Ok(rows) 205 + } 206 + 207 + /// Find drafts with stale or missing titles. 208 + /// 209 + /// Compares edit_heads to draft_titles to find drafts where the current 210 + /// head doesn't match the head used for title extraction. 211 + pub async fn get_stale_draft_titles(&self, limit: i64) -> Result<Vec<StaleDraftRow>, IndexError> { 212 + // Join drafts -> edit_heads (for current head) -> draft_titles (to check staleness) 213 + // edit_heads uses resource_type='draft' and resource_did/resource_rkey to link 214 + let query = r#" 215 + SELECT 216 + d.did, 217 + d.rkey, 218 + h.head_did, 219 + h.head_rkey, 220 + h.head_cid, 221 + h.root_did, 222 + h.root_rkey, 223 + h.root_cid 224 + FROM drafts d FINAL 225 + INNER JOIN edit_heads h FINAL 226 + ON h.resource_did = d.did 227 + AND h.resource_rkey = d.rkey 228 + AND h.resource_collection = 'sh.weaver.edit.draft' 229 + LEFT JOIN draft_titles t FINAL 230 + ON t.did = d.did 231 + AND t.rkey = d.rkey 232 + WHERE d.deleted_at = toDateTime64(0, 3) 233 + AND (t.head_cid IS NULL OR t.head_cid = '' OR t.head_cid != h.head_cid) 234 + LIMIT ? 235 + "#; 236 + 237 + let rows = self 238 + .inner() 239 + .query(query) 240 + .bind(limit) 241 + .fetch_all::<StaleDraftRow>() 242 + .await 243 + .map_err(|e| ClickHouseError::Query { 244 + message: "failed to get stale draft titles".into(), 245 + source: e, 246 + })?; 247 + 248 + Ok(rows) 249 + } 250 + 251 + /// Get the edit chain from head back to root for reconstructing a Loro doc. 252 + /// 253 + /// Returns nodes in order from root to head (for sequential application). 254 + /// Validates that the chain terminates at the expected root. 255 + pub async fn get_edit_chain( 256 + &self, 257 + root_did: &str, 258 + root_rkey: &str, 259 + head_did: &str, 260 + head_rkey: &str, 261 + ) -> Result<Vec<EditChainNode>, IndexError> { 262 + // Walk backwards from head to root via prev links 263 + // Use recursive CTE to traverse the chain, stopping when we hit the expected root 264 + let query = r#" 265 + WITH RECURSIVE chain AS ( 266 + -- Start from head 267 + SELECT did, rkey, cid, node_type, prev_did, prev_rkey, 0 as depth, 268 + (did = ? AND rkey = ?) as is_root 269 + FROM edit_nodes FINAL 270 + WHERE did = ? AND rkey = ? 271 + AND deleted_at = toDateTime64(0, 3) 272 + 273 + UNION ALL 274 + 275 + -- Follow prev links until we hit the root 276 + SELECT e.did, e.rkey, e.cid, e.node_type, e.prev_did, e.prev_rkey, c.depth + 1, 277 + (e.did = ? AND e.rkey = ?) as is_root 278 + FROM edit_nodes e FINAL 279 + INNER JOIN chain c ON e.did = c.prev_did AND e.rkey = c.prev_rkey 280 + WHERE e.deleted_at = toDateTime64(0, 3) 281 + AND c.is_root = 0 -- stop when we've reached the root 282 + AND c.depth < 1000 -- safety limit 283 + ) 284 + SELECT did, rkey, cid, node_type, prev_did, prev_rkey 285 + FROM chain 286 + ORDER BY depth DESC -- root first, then diffs in order 287 + "#; 288 + 289 + let rows = self 290 + .inner() 291 + .query(query) 292 + .bind(root_did) 293 + .bind(root_rkey) 294 + .bind(head_did) 295 + .bind(head_rkey) 296 + .bind(root_did) 297 + .bind(root_rkey) 298 + .fetch_all::<EditChainNode>() 299 + .await 300 + .map_err(|e| ClickHouseError::Query { 301 + message: "failed to get edit chain".into(), 302 + source: e, 303 + })?; 304 + 305 + // Validate chain terminates at expected root 306 + if let Some(first) = rows.first() { 307 + if first.did != root_did || first.rkey != root_rkey { 308 + return Err(ClickHouseError::Query { 309 + message: format!( 310 + "edit chain did not terminate at expected root {}:{}, got {}:{}", 311 + root_did, root_rkey, first.did, first.rkey 312 + ), 313 + source: clickhouse::error::Error::Custom("chain validation failed".into()), 314 + } 315 + .into()); 316 + } 317 + } 318 + 319 + Ok(rows) 320 + } 321 + 322 + /// Upsert a draft title after extraction. 323 + pub async fn upsert_draft_title( 324 + &self, 325 + did: &str, 326 + rkey: &str, 327 + title: &str, 328 + head_did: &str, 329 + head_rkey: &str, 330 + head_cid: &str, 331 + ) -> Result<(), IndexError> { 332 + let query = r#" 333 + INSERT INTO draft_titles (did, rkey, title, head_did, head_rkey, head_cid) 334 + VALUES (?, ?, ?, ?, ?, ?) 335 + "#; 336 + 337 + self.inner() 338 + .query(query) 339 + .bind(did) 340 + .bind(rkey) 341 + .bind(title) 342 + .bind(head_did) 343 + .bind(head_rkey) 344 + .bind(head_cid) 345 + .execute() 346 + .await 347 + .map_err(|e| ClickHouseError::Query { 348 + message: "failed to upsert draft title".into(), 349 + source: e, 350 + })?; 351 + 352 + Ok(()) 168 353 } 169 354 }
+2 -2
crates/weaver-index/src/clickhouse/queries/notebooks.rs
··· 163 163 e.indexed_at AS indexed_at, 164 164 e.record AS record 165 165 FROM notebook_entries ne FINAL 166 - INNER JOIN entries FINAL AS e ON 166 + INNER JOIN entries e FINAL ON 167 167 e.did = ne.entry_did 168 168 AND e.rkey = ne.entry_rkey 169 169 AND e.deleted_at = toDateTime64(0, 3) ··· 731 731 e.indexed_at AS indexed_at, 732 732 e.record AS record 733 733 FROM notebook_entries ne FINAL 734 - INNER JOIN entries FINAL AS e ON 734 + INNER JOIN entries e FINAL ON 735 735 e.did = ne.entry_did 736 736 AND e.rkey = ne.entry_rkey 737 737 AND e.deleted_at = toDateTime64(0, 3)
+8
crates/weaver-index/src/endpoints/edit.rs
··· 369 369 370 370 let last_edit_at = row.last_edit_at.map(|dt| Datetime::new(dt.fixed_offset())); 371 371 372 + // Include title if available 373 + let title = if row.title.is_empty() { 374 + None 375 + } else { 376 + Some(row.title.to_cowstr().into_static()) 377 + }; 378 + 372 379 drafts.push( 373 380 DraftView::new() 374 381 .uri(uri) ··· 376 383 .created_at(created_at) 377 384 .maybe_edit_root(edit_root) 378 385 .maybe_last_edit_at(last_edit_at) 386 + .maybe_title(title) 379 387 .build(), 380 388 ); 381 389 }
+18 -1
crates/weaver-index/src/endpoints/notebook.rs
··· 152 152 XrpcErrorResponse::internal_error("Invalid CID stored") 153 153 })?; 154 154 155 + let entry_contributors = state 156 + .clickhouse 157 + .get_entry_contributors(did_str, &entry_row.rkey) 158 + .await 159 + .map_err(|e| { 160 + tracing::error!("Failed to get entry contributors: {}", e); 161 + XrpcErrorResponse::internal_error("Database query failed") 162 + })?; 163 + 164 + let mut all_author_dids: HashSet<SmolStr> = entry_contributors.iter().cloned().collect(); 165 + // Also include author_dids from the record (explicit declarations) 166 + for did in &entry_row.author_dids { 167 + all_author_dids.insert(did.clone()); 168 + } 169 + 170 + let author_dids_vec: Vec<SmolStr> = all_author_dids.into_iter().collect(); 171 + 155 172 // Hydrate entry authors 156 - let entry_authors = hydrate_authors(&entry_row.author_dids, &profile_map)?; 173 + let entry_authors = hydrate_authors(&author_dids_vec, &profile_map)?; 157 174 158 175 // Parse record JSON 159 176 let entry_record = parse_record_json(&entry_row.record)?;
+4
crates/weaver-index/src/error.rs
··· 29 29 #[error(transparent)] 30 30 #[diagnostic(transparent)] 31 31 Sqlite(#[from] SqliteError), 32 + 33 + #[error("resource not found: {resource}")] 34 + #[diagnostic(code(index::not_found))] 35 + NotFound { resource: String }, 32 36 } 33 37 34 38 /// HTTP server errors
+2
crates/weaver-index/src/lib.rs
··· 9 9 pub mod service_identity; 10 10 pub mod sqlite; 11 11 pub mod tap; 12 + pub mod tasks; 12 13 13 14 pub use config::Config; 14 15 pub use error::{IndexError, Result}; ··· 17 18 pub use server::{AppState, ServerConfig}; 18 19 pub use service_identity::ServiceIdentity; 19 20 pub use sqlite::{ShardKey, ShardRouter, SqliteShard}; 21 + pub use tasks::{run_draft_title_task, DraftTitleTaskConfig};
+380
crates/weaver-index/src/tasks/draft_titles.rs
··· 1 + //! Background task for extracting draft titles from Loro snapshots. 2 + //! 3 + //! Periodically scans for drafts where the edit head has changed since 4 + //! the last title extraction, fetches the edit chain from PDS, reconstructs 5 + //! the Loro document, and extracts the title. 6 + 7 + use std::sync::Arc; 8 + use std::time::Duration; 9 + 10 + use jacquard::client::UnauthenticatedSession; 11 + use jacquard::identity::JacquardResolver; 12 + use jacquard::prelude::{IdentityResolver, XrpcExt}; 13 + use jacquard::types::ident::AtIdentifier; 14 + use jacquard::types::string::{Cid, Did}; 15 + use loro::LoroDoc; 16 + use mini_moka::sync::Cache; 17 + use tracing::{debug, error, info, warn}; 18 + 19 + use crate::clickhouse::{Client, StaleDraftRow}; 20 + use crate::error::IndexError; 21 + 22 + use weaver_api::com_atproto::repo::get_record::GetRecord; 23 + use weaver_api::com_atproto::sync::get_blob::GetBlob; 24 + use weaver_api::sh_weaver::edit::diff::Diff; 25 + use weaver_api::sh_weaver::edit::root::Root; 26 + 27 + /// Cache for PDS blob fetches. 28 + /// 29 + /// Blobs are content-addressed so safe to cache indefinitely. 30 + /// Key is (did, cid) as a string. 31 + #[derive(Clone)] 32 + pub struct BlobCache { 33 + cache: Cache<String, Arc<Vec<u8>>>, 34 + } 35 + 36 + impl BlobCache { 37 + pub fn new(max_capacity: u64) -> Self { 38 + Self { 39 + cache: Cache::new(max_capacity), 40 + } 41 + } 42 + 43 + fn key(did: &str, cid: &str) -> String { 44 + format!("{}:{}", did, cid) 45 + } 46 + 47 + pub fn get(&self, did: &str, cid: &str) -> Option<Arc<Vec<u8>>> { 48 + self.cache.get(&Self::key(did, cid)) 49 + } 50 + 51 + pub fn insert(&self, did: &str, cid: &str, data: Vec<u8>) { 52 + self.cache.insert(Self::key(did, cid), Arc::new(data)); 53 + } 54 + } 55 + 56 + /// Configuration for the draft title extraction task 57 + #[derive(Debug, Clone)] 58 + pub struct DraftTitleTaskConfig { 59 + /// How often to check for stale titles 60 + pub interval: Duration, 61 + /// Maximum drafts to process per run 62 + pub batch_size: i64, 63 + } 64 + 65 + impl Default for DraftTitleTaskConfig { 66 + fn default() -> Self { 67 + Self { 68 + interval: Duration::from_secs(120), // 2 minutes 69 + batch_size: 50, 70 + } 71 + } 72 + } 73 + 74 + /// Run the draft title extraction task in a loop 75 + pub async fn run_draft_title_task( 76 + client: Arc<Client>, 77 + resolver: UnauthenticatedSession<JacquardResolver>, 78 + config: DraftTitleTaskConfig, 79 + ) { 80 + info!( 81 + interval_secs = config.interval.as_secs(), 82 + batch_size = config.batch_size, 83 + "starting draft title extraction task" 84 + ); 85 + 86 + // Cache for blob fetches - blobs are content-addressed, safe to cache indefinitely 87 + // 1000 entries is plenty for typical edit chains 88 + let blob_cache = BlobCache::new(1000); 89 + 90 + loop { 91 + match process_stale_drafts(&client, &resolver, &blob_cache, config.batch_size).await { 92 + Ok(count) => { 93 + if count > 0 { 94 + info!(processed = count, "draft title extraction complete"); 95 + } else { 96 + debug!("no stale draft titles to process"); 97 + } 98 + } 99 + Err(e) => { 100 + error!(error = ?e, "draft title extraction failed"); 101 + } 102 + } 103 + 104 + tokio::time::sleep(config.interval).await; 105 + } 106 + } 107 + 108 + /// Process a batch of stale drafts 109 + async fn process_stale_drafts( 110 + client: &Client, 111 + resolver: &UnauthenticatedSession<JacquardResolver>, 112 + blob_cache: &BlobCache, 113 + batch_size: i64, 114 + ) -> Result<usize, IndexError> { 115 + let stale = client.get_stale_draft_titles(batch_size).await?; 116 + 117 + if stale.is_empty() { 118 + return Ok(0); 119 + } 120 + 121 + debug!(count = stale.len(), "found stale draft titles"); 122 + 123 + let mut processed = 0; 124 + for draft in stale { 125 + match extract_and_save_title(client, resolver, blob_cache, &draft).await { 126 + Ok(title) => { 127 + debug!( 128 + did = %draft.did, 129 + rkey = %draft.rkey, 130 + title = %title, 131 + "extracted draft title" 132 + ); 133 + processed += 1; 134 + } 135 + Err(e) => { 136 + warn!( 137 + did = %draft.did, 138 + rkey = %draft.rkey, 139 + error = ?e, 140 + "failed to extract draft title" 141 + ); 142 + } 143 + } 144 + } 145 + 146 + Ok(processed) 147 + } 148 + 149 + /// Extract title from a single draft and save it 150 + async fn extract_and_save_title( 151 + client: &Client, 152 + resolver: &UnauthenticatedSession<JacquardResolver>, 153 + blob_cache: &BlobCache, 154 + draft: &StaleDraftRow, 155 + ) -> Result<String, IndexError> { 156 + // Get the edit chain from ClickHouse 157 + let chain = client 158 + .get_edit_chain( 159 + &draft.root_did, 160 + &draft.root_rkey, 161 + &draft.head_did, 162 + &draft.head_rkey, 163 + ) 164 + .await?; 165 + 166 + if chain.is_empty() { 167 + return Err(IndexError::NotFound { 168 + resource: format!("edit chain for {}:{}", draft.did, draft.rkey), 169 + }); 170 + } 171 + 172 + // Resolve PDS for the root DID 173 + let root_did = Did::new(&draft.root_did).map_err(|e| IndexError::NotFound { 174 + resource: format!("invalid root DID: {}", e), 175 + })?; 176 + 177 + let pds_url = resolver 178 + .pds_for_did(&root_did) 179 + .await 180 + .map_err(|e| IndexError::NotFound { 181 + resource: format!("PDS for {}: {}", root_did, e), 182 + })?; 183 + 184 + // Initialize Loro doc 185 + let doc = LoroDoc::new(); 186 + 187 + // Process chain: first node should be root, rest are diffs 188 + for (i, node) in chain.iter().enumerate() { 189 + let node_did = Did::new(&node.did).map_err(|e| IndexError::NotFound { 190 + resource: format!("invalid node DID: {}", e), 191 + })?; 192 + 193 + if node.node_type == "root" { 194 + // Fetch root record 195 + let root_record = 196 + fetch_root_record(resolver, pds_url.clone(), &node_did, &node.rkey).await?; 197 + 198 + // Fetch snapshot blob 199 + let snapshot_cid = root_record.snapshot.blob().cid(); 200 + let snapshot_bytes = 201 + fetch_blob(resolver, blob_cache, pds_url.clone(), &node_did, snapshot_cid).await?; 202 + 203 + // Import snapshot 204 + doc.import(&snapshot_bytes) 205 + .map_err(|e| IndexError::NotFound { 206 + resource: format!("failed to import root snapshot: {}", e), 207 + })?; 208 + 209 + debug!( 210 + did = %node.did, 211 + rkey = %node.rkey, 212 + bytes = snapshot_bytes.len(), 213 + "imported root snapshot" 214 + ); 215 + } else { 216 + // Fetch diff record 217 + let diff_record = 218 + fetch_diff_record(resolver, pds_url.clone(), &node_did, &node.rkey).await?; 219 + 220 + // Diffs can have inline diff bytes or a snapshot blob reference 221 + let diff_bytes = if let Some(ref inline) = diff_record.inline_diff { 222 + // Use inline diff (base64 decoded by serde) 223 + inline.to_vec() 224 + } else if let Some(ref snapshot_blob) = diff_record.snapshot { 225 + // Fetch snapshot blob 226 + let snapshot_cid = snapshot_blob.blob().cid(); 227 + fetch_blob(resolver, blob_cache, pds_url.clone(), &node_did, snapshot_cid).await? 228 + } else { 229 + warn!( 230 + did = %node.did, 231 + rkey = %node.rkey, 232 + "diff has neither inline nor snapshot data, skipping" 233 + ); 234 + continue; 235 + }; 236 + 237 + // Import diff 238 + doc.import(&diff_bytes).map_err(|e| IndexError::NotFound { 239 + resource: format!("failed to import diff {}: {}", i, e), 240 + })?; 241 + 242 + debug!( 243 + did = %node.did, 244 + rkey = %node.rkey, 245 + bytes = diff_bytes.len(), 246 + "imported diff" 247 + ); 248 + } 249 + } 250 + 251 + // Extract title from Loro doc 252 + let title = doc.get_text("title").to_string(); 253 + 254 + // Save to ClickHouse 255 + client 256 + .upsert_draft_title( 257 + &draft.did, 258 + &draft.rkey, 259 + &title, 260 + &draft.head_did, 261 + &draft.head_rkey, 262 + &draft.head_cid, 263 + ) 264 + .await?; 265 + 266 + Ok(title) 267 + } 268 + 269 + /// Fetch an edit.root record from PDS 270 + async fn fetch_root_record( 271 + resolver: &UnauthenticatedSession<JacquardResolver>, 272 + pds_url: jacquard::url::Url, 273 + did: &Did<'_>, 274 + rkey: &str, 275 + ) -> Result<Root<'static>, IndexError> { 276 + use jacquard::IntoStatic; 277 + use jacquard::types::string::Nsid; 278 + 279 + let request = GetRecord::new() 280 + .repo(AtIdentifier::Did(did.clone())) 281 + .collection(Nsid::new_static("sh.weaver.edit.root").unwrap()) 282 + .rkey( 283 + jacquard::types::recordkey::RecordKey::any(rkey).map_err(|e| IndexError::NotFound { 284 + resource: format!("invalid rkey: {}", e), 285 + })?, 286 + ) 287 + .build(); 288 + 289 + let response = 290 + resolver 291 + .xrpc(pds_url) 292 + .send(&request) 293 + .await 294 + .map_err(|e| IndexError::NotFound { 295 + resource: format!("root record {}/{}: {}", did, rkey, e), 296 + })?; 297 + 298 + let output = response.into_output().map_err(|e| IndexError::NotFound { 299 + resource: format!("parse root record: {}", e), 300 + })?; 301 + 302 + let root: Root = jacquard::from_data(&output.value).map_err(|e| IndexError::NotFound { 303 + resource: format!("deserialize root: {}", e), 304 + })?; 305 + 306 + Ok(root.into_static()) 307 + } 308 + 309 + /// Fetch an edit.diff record from PDS 310 + async fn fetch_diff_record( 311 + resolver: &UnauthenticatedSession<JacquardResolver>, 312 + pds_url: jacquard::url::Url, 313 + did: &Did<'_>, 314 + rkey: &str, 315 + ) -> Result<Diff<'static>, IndexError> { 316 + use jacquard::IntoStatic; 317 + use jacquard::types::string::Nsid; 318 + 319 + let request = GetRecord::new() 320 + .repo(AtIdentifier::Did(did.clone())) 321 + .collection(Nsid::new_static("sh.weaver.edit.diff").unwrap()) 322 + .rkey( 323 + jacquard::types::recordkey::RecordKey::any(rkey).map_err(|e| IndexError::NotFound { 324 + resource: format!("invalid rkey: {}", e), 325 + })?, 326 + ) 327 + .build(); 328 + 329 + let response = 330 + resolver 331 + .xrpc(pds_url) 332 + .send(&request) 333 + .await 334 + .map_err(|e| IndexError::NotFound { 335 + resource: format!("diff record {}/{}: {}", did, rkey, e), 336 + })?; 337 + 338 + let output = response.into_output().map_err(|e| IndexError::NotFound { 339 + resource: format!("parse diff record: {}", e), 340 + })?; 341 + 342 + let diff: Diff = jacquard::from_data(&output.value).map_err(|e| IndexError::NotFound { 343 + resource: format!("deserialize diff: {}", e), 344 + })?; 345 + 346 + Ok(diff.into_static()) 347 + } 348 + 349 + /// Fetch a blob from PDS, using cache when available 350 + async fn fetch_blob( 351 + resolver: &UnauthenticatedSession<JacquardResolver>, 352 + cache: &BlobCache, 353 + pds_url: jacquard::url::Url, 354 + did: &Did<'_>, 355 + cid: &Cid<'_>, 356 + ) -> Result<Vec<u8>, IndexError> { 357 + // Check cache first - blobs are content-addressed 358 + if let Some(cached) = cache.get(did.as_str(), cid.as_str()) { 359 + debug!(cid = %cid, "blob cache hit"); 360 + return Ok(cached.as_ref().clone()); 361 + } 362 + 363 + let request = GetBlob::new().did(did.clone()).cid(cid.clone()).build(); 364 + 365 + let response = 366 + resolver 367 + .xrpc(pds_url) 368 + .send(&request) 369 + .await 370 + .map_err(|e| IndexError::NotFound { 371 + resource: format!("blob {}: {}", cid, e), 372 + })?; 373 + 374 + let bytes = response.buffer().to_vec(); 375 + 376 + // Cache for future use 377 + cache.insert(did.as_str(), cid.as_str(), bytes.clone()); 378 + 379 + Ok(bytes) 380 + }
+5
crates/weaver-index/src/tasks/mod.rs
··· 1 + //! Background tasks for the indexer 2 + 3 + mod draft_titles; 4 + 5 + pub use draft_titles::{run_draft_title_task, DraftTitleTaskConfig};
+6 -5
weaver_notes/.obsidian/workspace.json
··· 41 41 "state": { 42 42 "type": "markdown", 43 43 "state": { 44 - "file": "Writing the AppView Last.md", 44 + "file": "Untitled.md", 45 45 "mode": "source", 46 46 "source": false 47 47 }, 48 48 "icon": "lucide-file", 49 - "title": "Writing the AppView Last" 49 + "title": "Untitled" 50 50 } 51 51 } 52 52 ], ··· 199 199 "bases:Create new base": false 200 200 } 201 201 }, 202 - "active": "6029beecc3d03bce", 202 + "active": "2f6bdb6d2dce13ed", 203 203 "lastOpenFiles": [ 204 + "cargo_bloated.png", 205 + "Writing the AppView Last.md", 206 + "Untitled.md", 204 207 "weaver_index_html.png", 205 208 "2025-12-14T23:32:19.png", 206 209 "diff_record.png", 207 210 "Why I rewrote pdsls in Rust (tm).md", 208 - "Writing the AppView Last.md", 209 211 "light_mode_excerpt.png", 210 212 "notebook_entry_preview.png", 211 213 "xkcd_345_excerpt.png", ··· 214 216 "Pasted image 20251114125028.png", 215 217 "invalid_record.png", 216 218 "Pasted image 20251114125031.png", 217 - "Pasted image 20251114121431.png", 218 219 "Arch.md", 219 220 "Weaver - Long-form writing.md" 220 221 ]
+256
weaver_notes/Untitled.md
··· 1 + I recently used Jacquard to write an ~AppView~ Index for Weaver. I alluded in my posts about my devlog about that experience how easy I had made the actual web server side of that. Lexicon as a specification language provides a lot of ways to specify data types and a few to specify API endpoints. XRPC is the canonical way to do that, and it's an opinionated subset of HTTP, which narrows down to a specific endpoint format and set of "verbs". Your path is `/xrpc/your.lexicon.nsidEndpoint?argument=value`, your bodies are mostly JSON. 2 + 3 + I'm going to lead off by tooting someone else's horn. Chad Miller's https://quickslice.slices.network/ provides an excellent example of the kind of thing you can do with atproto lexicons, and it doesn't use XRPC at all, but instead generates GraphQL's equivalents. This is more freeform, requires less of you upfront, and is in a lot of ways more granular than XRPC could possibly allow. Jacquard is for the moment built around the expectations of XRPC. If someone want's Jacquard support for GraphQL on atproto lexicons, I'm all ears, though. 4 + 5 + Here's to me one of the benefits of XRPC, and one of the challenges. XRPC only specifies your inputs and your output. everything else between you need to figure out. This means more work, but it also means you have internal flexibility. And Jacquard's server-side XRPC helpers follow that. Jacquard XRPC code generation itself provides the output type and the errors. For the server side it generates one additional marker type, generally labeled `YourXrpcQueryRequest`, and a trait implementation for `XrpcEndpoint`. You can also get these with `derive(XrpcRequest)` on existing Rust structs without writing out lexicon JSON. 6 + 7 + ```rust 8 + pub trait XrpcEndpoint { 9 + /// Fully-qualified path ('/xrpc/\[nsid\]') where this endpoint should live on the server 10 + const PATH: &'static str; 11 + /// XRPC method (query/GET or procedure/POST) 12 + const METHOD: XrpcMethod; 13 + /// XRPC Request data type 14 + type Request<'de>: XrpcRequest + Deserialize<'de> + IntoStatic; 15 + /// XRPC Response data type 16 + type Response: XrpcResp; 17 + } 18 + 19 + /// Endpoint type for 20 + ///sh.weaver.actor.getActorNotebooks 21 + pub struct GetActorNotebooksRequest; 22 + impl XrpcEndpoint for GetActorNotebooksRequest { 23 + const PATH: &'static str = "/xrpc/sh.weaver.actor.getActorNotebooks"; 24 + const METHOD: XrpcMethod = XrpcMethod::Query; 25 + type Request<'de> = GetActorNotebooks<'de>; 26 + type Response = GetActorNotebooksResponse; 27 + } 28 + ``` 29 + 30 + As with many Jacquard traits you see the associated types carrying the lifetime. You may ask, why a second struct and trait? This is very similar to the `XrpcRequest` trait, which is implemented on the request struct itself, after all. 31 + 32 + ```rust 33 + impl<'a> XrpcRequest for GetActorNotebooks<'a> { 34 + const NSID: &'static str = "sh.weaver.actor.getActorNotebooks"; 35 + const METHOD: XrpcMethod = XrpcMethod::Query; 36 + type Response = GetActorNotebooksResponse; 37 + } 38 + ``` 39 + 40 + ## Time for magic 41 + The reason is that lifetime when combined with the constraints Axum puts on extractors. Because the request type includes a lifetime, if we were to attempt to implement `FromRequest` directly for `XrpcRequest`, the trait would require that `XrpcRequest` be implemented for all lifetimes, and also apply an effective `DeserializeOwned` bound, even if we were to specify the `'static` lifetime as we do. And of course `XrpcRequest` is implemented for one specific lifetime, `'a`, the lifetime of whatever it's borrowed from. Meanwhile `XrpcEndpoint` has no lifetime itself, but instead carries the lifetime on the `Request` associated type. This allows us to do the following implementation, where `ExtractXrpc<E>` has no lifetime itself and contains an owned version of the deserialized request. And we can then implement `FromRequest` for `ExtractXrpc<R>`, and put the `for<'any>` bound on the `IntoStatic` trait requirement in a where clause, where it works perfectly. In combination with the code generation in `jacquard-lexicon`, this is the full implementation of Jacquard's Axum XRPC request extractor. Not so bad. 42 + 43 + ```rust 44 + pub struct ExtractXrpc<E: XrpcEndpoint>(pub E::Request<'static>); 45 + 46 + impl<S, R> FromRequest<S> for ExtractXrpc<R> 47 + where 48 + S: Send + Sync, 49 + R: XrpcEndpoint, 50 + for<'a> R::Request<'a>: IntoStatic<Output = R::Request<'static>>, 51 + { 52 + type Rejection = Response; 53 + 54 + fn from_request( 55 + req: Request, 56 + state: &S, 57 + ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send { 58 + async { 59 + match R::METHOD { 60 + XrpcMethod::Procedure(_) => { 61 + let body = Bytes::from_request(req, state) 62 + .await 63 + .map_err(IntoResponse::into_response)?; 64 + let decoded = R::Request::decode_body(&body); 65 + match decoded { 66 + Ok(value) => Ok(ExtractXrpc(*value.into_static())), 67 + Err(err) => Err(( 68 + StatusCode::BAD_REQUEST, 69 + Json(json!({ 70 + "error": "InvalidRequest", 71 + "message": format!("failed to decode request: {}", err) 72 + })), 73 + ).into_response()), 74 + } 75 + } 76 + XrpcMethod::Query => { 77 + if let Some(path_query) = req.uri().path_and_query() { 78 + let query = path_query.query().unwrap_or(""); 79 + let value: R::Request<'_> = 80 + serde_html_form::from_str::<R::Request<'_>>(query).map_err(|e| { 81 + ( 82 + StatusCode::BAD_REQUEST, 83 + Json(json!({ 84 + "error": "InvalidRequest", 85 + "message": format!("failed to decode request: {}", e) 86 + })), 87 + ).into_response() 88 + })?; 89 + Ok(ExtractXrpc(value.into_static())) 90 + } else { 91 + Err(( 92 + StatusCode::BAD_REQUEST, 93 + Json(json!({ 94 + "error": "InvalidRequest", 95 + "message": "wrong path" 96 + })), 97 + ).into_response()) 98 + } 99 + } 100 + } 101 + } 102 + } 103 + ``` 104 + 105 + Jacquard then also provides an additional utility to round things out, using the associated `PATH` constant to put the handler for your XRPC request at the right spot in your router. 106 + ```rust 107 + /// Conversion trait to turn an XrpcEndpoint and a handler into an axum Router 108 + pub trait IntoRouter { 109 + fn into_router<T, S, U>(handler: U) -> Router<S> 110 + where 111 + T: 'static, 112 + S: Clone + Send + Sync + 'static, 113 + U: axum::handler::Handler<T, S>; 114 + } 115 + 116 + impl<X> IntoRouter for X 117 + where 118 + X: XrpcEndpoint, 119 + { 120 + /// Creates an axum router that will invoke `handler` in response to xrpc 121 + /// request `X`. 122 + fn into_router<T, S, U>(handler: U) -> Router<S> 123 + where 124 + T: 'static, 125 + S: Clone + Send + Sync + 'static, 126 + U: axum::handler::Handler<T, S>, 127 + { 128 + Router::new().route( 129 + X::PATH, 130 + (match X::METHOD { 131 + XrpcMethod::Query => axum::routing::get, 132 + XrpcMethod::Procedure(_) => axum::routing::post, 133 + })(handler), 134 + ) 135 + } 136 + } 137 + ``` 138 + 139 + Which then lets the Axum router for Weaver's Index look like this (truncated for length): 140 + 141 + ```rust 142 + pub fn router(state: AppState, did_doc: DidDocument<'static>) -> Router { 143 + Router::new() 144 + .route("/", get(landing)) 145 + .route( 146 + "/assets/IoskeleyMono-Regular.woff2", 147 + get(font_ioskeley_regular), 148 + ) 149 + .route("/assets/IoskeleyMono-Bold.woff2", get(font_ioskeley_bold)) 150 + .route( 151 + "/assets/IoskeleyMono-Italic.woff2", 152 + get(font_ioskeley_italic), 153 + ) 154 + .route("/xrpc/_health", get(health)) 155 + .route("/metrics", get(metrics)) 156 + // com.atproto.identity.* endpoints 157 + .merge(ResolveHandleRequest::into_router(identity::resolve_handle)) 158 + // com.atproto.repo.* endpoints (record cache) 159 + .merge(GetRecordRequest::into_router(repo::get_record)) 160 + .merge(ListRecordsRequest::into_router(repo::list_records)) 161 + // app.bsky.* passthrough endpoints 162 + .merge(BskyGetProfileRequest::into_router(bsky::get_profile)) 163 + .merge(BskyGetPostsRequest::into_router(bsky::get_posts)) 164 + // sh.weaver.actor.* endpoints 165 + .merge(GetProfileRequest::into_router(actor::get_profile)) 166 + .merge(GetActorNotebooksRequest::into_router( 167 + actor::get_actor_notebooks, 168 + )) 169 + .merge(GetActorEntriesRequest::into_router( 170 + actor::get_actor_entries, 171 + )) 172 + // sh.weaver.notebook.* endpoints 173 + ... 174 + // sh.weaver.collab.* endpoints 175 + ... 176 + // sh.weaver.edit.* endpoints 177 + ... 178 + .layer(TraceLayer::new_for_http()) 179 + .layer(CorsLayer::permissive() 180 + .max_age(std::time::Duration::from_secs(86400)) 181 + ).with_state(state) 182 + .merge(did_web_router(did_doc)) 183 + } 184 + ``` 185 + 186 + Each of the handlers is a fairly straightforward async function that takes `AppState`, the XrpcExtractor, and an extractor and validator for service auth, which allows it to be accessed through via your PDS via the `atproto-proxy` header, and return user-specific data, or gate specific endpoints as requiring authentication. 187 + 188 + > And so yeah, the actual HTTP server part of the index was dead-easy to write. The handlers themselves are some of them fairly *long* functions, as they need to pull together the required data from the database over a couple of queries and then do some conversion, but they're straightforward. At some point I may end up either adding additional specialized view tables to the database or rewriting my queries to do more in SQL or both, but for now it made sense to keep the final decision-making and assembly in Rust, where it's easier to iterate on. 189 + ### Service Auth 190 + Service Auth is, for those not familiar, the non-OAuth way to talk to an XRPC server other than your PDS with an authenticated identity. It's the method the Bluesky AppView uses. There are some downsides to proxying through the PDS, like delay in being able to read your own writes without some PDS-side or app-level handling, but it is conceptually very simple. The PDS, when it pipes through an XRPC request to another service, validates authentication, then generates a short-lived JWT, signs it with the user's private key, and puts it in a header. The service then extracts that, decodes it, and validates it using the public key in the user's DID document. Jacquard provides a middleware that can be used to gate routes based on service auth validation and it also provides an extractor. Initially I provided just one where authentication is required, but as part of building the index I added an additional one for optional authentication, where the endpoint is public, but returns user-specific information when there is an authenticated user. It returns this structure. 191 + 192 + ```rust 193 + #[derive(Debug, Clone, jacquard_derive::IntoStatic)] 194 + pub struct VerifiedServiceAuth<'a> { 195 + /// The authenticated user's DID (from `iss` claim) 196 + did: Did<'a>, 197 + /// The audience (should match your service DID) 198 + aud: Did<'a>, 199 + /// The lexicon method NSID, if present 200 + lxm: Option<Nsid<'a>>, 201 + /// JWT ID (nonce), if present 202 + jti: Option<CowStr<'a>>, 203 + } 204 + ``` 205 + 206 + Ultimately I want to provide a similar set of OAuth extractors as well, but those need to be built, still. If I move away from service proxying for the Weaver index, they will definitely get written at that point. 207 + 208 + > I mentioned some bug-fixing in Jacquard was required to make this work. There were a couple of oversights in the `DidDocument` struct and a spot I had incorrectly held a tracing span across an await point. Also, while using the `slingshot_resolver` set of options for `JacquardResolver` is great under normal circumstances (and normally I default to it), the mini-doc does NOT in fact include the signing keys, and cannot be used to validate service auth. 209 + > 210 + > I am not always a smart woman. 211 + 212 + ## Why not go full magic? 213 + One thing the Jacquard service auth validation extractor does **not** provide is validation of that jti nonce. That is left as an exercise for the server developer, to maintain a cache of recent nonces and compare against them. I leave a number of things this way, and this is deliberate. I think this is the correct approach. As powerful as "magic" all-in-one frameworks like Dioxus (or the various full-stack JS frameworks) are, the magic usually ends up constraining you in a number of ways. There are a number of awkward things in the front-end app implementation which are downstream of constraints Dioxus applies to your types and functions in order to work its magic. 214 + 215 + There are a lot of possible things you might want to do as an XRPC server. You might be a PDS, you might be an AppView or index, you might be some other sort of service that doesn't really fit into the boxes (like a Tangled knot server or Streamplace node) you might authenticate via service auth or OAuth, communicate via the PDS or directly with the client app. And as such, while my approach to everything in Jacquard is to provide a comprehensive box of tools rather than a complete end-to-end solution, this is especially true on the server side of things, because of that diversity in requirements, and my desire to not constrain developers using the library to work a certain way, so that they can build anything they want on atproto. 216 + 217 + > If you haven't read the Not An AppView entry, here it is. I might recommend reading it, and some other previous entries in that notebook, as it will help put the following in context. 218 + 219 + ![[at://did:plc:yfvwmnlztr4dwkb7hwz55r2g/sh.weaver.notebook.entry/3m7ysqf2z5s22]] 220 + ## Dogfooding again 221 + That being said, my experience writing the Weaver front-end and now the index server does leave me wanting a few things. One is a "BFF" session type, which forwards requests through a server to the PDS (or index), acting somewhat like [oatproxy](https://github.com/streamplace/oatproxy) (prototype jacquard version of that [here](https://github.com/espeon/istat/tree/main/jacquard-oatproxy) courtesy of Nat and Claude). This allows easier reading of your own writes via server-side caching, some caching and deduplication of common requests to reduce load on the PDS and roundtrip time. If the seession lives server-side it allows longer-lived confidential sessions for OAuth, and avoids putting OAuth tokens on the client device. 222 + 223 + Once implemented, I will likely refactor the Weaver app to use this session type in fullstack-server mode, which will then help dramatically simplify a bunch of client-side code. The refactored app will likely include an internal XRPC "server" of sorts that will elide differences between the index's XRPC APIs and the index-less flow. With the "fullstack-server" and "use-index" features, the client app running in the browser will forward authenticated requests through the app server to the index or PDS. With "fullstack-server" only, the app server itself acts like a discount version of the index, implemented via generic services like Constellation. Performance will be significantly improved over the original index-less implementation due to better caching, and unifying the cache. In client-only mode there are a couple of options, and I am not sure which is ultimately correct. The straightforward way as far as separation of concerns goes would be to essentially use a web worker as intermediary and local cache. That worker would be compiled to either use the index or to make Constellation and direct PDS requests, depending on the "use-index" feature. However that brings with it the obvious overhead of copying data from the worker to the app in the default mode, and I haven't yet investigated how feasible the available options which might allow zero-copy transfer via SharedArrayBuffer are. That being said, the real-time collaboration feature already works this way (sans SharedArrayBuffer) and lag is comparable to when the iroh connection was handled in the UI thread. 224 + 225 + A fair bit of this is somewhat new territory for me, when it comes to the browser, and I would be ***very*** interested in hearing from people with more domain experience on the likely correct approach. 226 + 227 + On that note, one of my main frustrations with Jacquard as a library is how heavy it is in terms of compiled binary size due to monomorphization. I made that choice, to do everything via static dispatch, but when you want to ship as small a binary as possible over the network, it works against you. On WASM I haven't gotten a great number of exactly the granular damage, but on x86_64 (albeit with less aggressive optimisation for size) we're talking kilobytes of pure duplicated functions for every jacquard type used in the application, plus whatever else. 228 + ```rust 229 + 0.0% 0.0% 9.3KiB weaver_app weaver_app::components::editor::sync::create_diff::{closure#0} 230 + 0.0% 0.0% 9.2KiB loro_internal <loro_internal::txn::Transaction>::_commit 231 + 0.0% 0.0% 9.2KiB weaver_app <weaver_app::fetch::Fetcher as jacquard::client::AgentSessionExt>::get_record::<weaver_api::sh_weaver::collab::invite::Invite>::{closure#0} 232 + 0.0% 0.0% 9.2KiB weaver_app <weaver_app::fetch::Fetcher as jacquard::client::AgentSessionExt>::get_record::<weaver_api::sh_weaver::actor::profile::ProfileRecord>::{closure#0} 233 + 0.0% 0.0% 9.2KiB weaver_app <weaver_app::fetch::Fetcher as jacquard::client::AgentSessionExt>::get_record::<weaver_api::app_bsky::actor::profile::ProfileRecord>::{closure#0} 234 + 0.0% 0.0% 9.2KiB weaver_renderer <jacquard_identity::JacquardResolver as jacquard_identity::resolver::IdentityResolver>::resolve_did_doc::{closure#0}::{closure#0} 235 + 0.0% 0.0% 9.2KiB weaver_app <weaver_app::fetch::Client as jacquard::client::AgentSessionExt>::get_record::<weaver_api::sh_weaver::notebook::theme::Theme>::{closure#0} 236 + 0.0% 0.0% 9.2KiB weaver_app <weaver_app::fetch::Client as jacquard::client::AgentSessionExt>::get_record::<weaver_api::sh_weaver::notebook::entry::Entry>::{closure#0} 237 + 0.0% 0.0% 9.2KiB weaver_app <weaver_app::fetch::Client as jacquard::client::AgentSessionExt>::get_record::<weaver_api::sh_weaver::notebook::book::Book>::{closure#0} 238 + 0.0% 0.0% 9.2KiB weaver_app <weaver_app::fetch::Client as jacquard::client::AgentSessionExt>::get_record::<weaver_api::sh_weaver::notebook::colour_scheme::ColourScheme>::{closure#0} 239 + 0.0% 0.0% 9.2KiB weaver_app <weaver_app::fetch::Client as jacquard::client::AgentSessionExt>::get_record::<weaver_api::sh_weaver::actor::profile::ProfileRecord>::{closure#0} 240 + 0.0% 0.0% 9.2KiB weaver_app <weaver_app::fetch::Client as jacquard::client::AgentSessionExt>::get_record::<weaver_api::sh_weaver::edit::draft::Draft>::{closure#0} 241 + 0.0% 0.0% 9.2KiB weaver_app <weaver_app::fetch::Client as jacquard::client::AgentSessionExt>::get_record::<weaver_api::sh_weaver::edit::root::Root>::{closure#0} 242 + 0.0% 0.0% 9.2KiB weaver_app <weaver_app::fetch::Client as jacquard::client::AgentSessionExt>::get_record::<weaver_api::sh_weaver::edit::diff::Diff>::{closure#0} 243 + 0.0% 0.0% 9.2KiB weaver_app <weaver_app::fetch::Client as jacquard::client::AgentSessionExt>::get_record::<weaver_api::app_bsky::actor::profile::ProfileRecord>::{closure#0} 244 + 0.0% 0.0% 9.2KiB resvg <image_webp::vp8::Vp8Decoder<std::io::Take<&mut std::io::cursor::Cursor<&[u8]>>>>::loop_filter 245 + 0.0% 0.0% 9.2KiB miette <miette::handlers::graphical::GraphicalReportHandler>::render_context::<alloc::string::String> 246 + 0.0% 0.0% 9.1KiB miette <miette::handlers::graphical::GraphicalReportHandler>::render_context::<core::fmt::Formatter> 247 + 0.0% 0.0% 9.1KiB weaver_app weaver_app::components::record_editor::EditableRecordContent::{closure#7}::{closure#0} 248 + ``` 249 + 250 + I've taken a couple stabs at refactors to help with this, but haven't found a solution that satisfies me, in part because one of the problems in practice is of course overhead from `serde_json` monomorphization. Unfortunately, the alternatives trade off in frustrating ways. [`facet`](https://github.com/facet-rs/facet) has its own binary size impacts and `facet-json` is missing a couple of critical features to work with atproto JSON data (internally-tagged enums, most notably). Something like [`simd-json`](https://github.com/simd-lite/simd-json) or [`serde_json_borrow`](https://github.com/PSeitz/serde_json_borrow) is fast and can borrow from the buffer in a way that is very useful to us (and honestly I intend to swap to them for some uses at some point), but `serde_json_borrow` only provides a value type, and I would then be uncertain at the monomorphization overhead of transforming that type into jacquard types. The `serde` implementation for `simd-json` is heavily based on `serde_json` and thus likely has much the same overhead problem. And [`miniserde`](https://github.com/dtolnay/miniserde) similarly lacks support for parts of JSON that atproto data requires (enums again). And writing my own custom JSON parser that deserializes into Jacquard's `Data` or `RawData` types (from where it can then be deserialized more simply into concrete types, ideally with much less code duplication) is not a project I have time for, and is on the tedious side of the kind of thing I enjoy, particularly the process of ensuring it is sufficiently robust for real-world use, and doesn't perform terribly. 251 + 252 + `dyn` compatibility for some of the Jacquard traits is possible but comes with its own challenges, as currently `Serialize` is a supertrait of `XrpcRequest`, and rewriting around removing that bound that is both a nontrivial refactor (and a breaking API change, and it's not the only barrier to dyn compatibility) and may not actually reduce the number of copies of `get_record()` in the binary as much as one would hope. Now, if most of the code could be taken out of that and put into a function that could be totally shared between all implementations or at least most, that would be ideal but the solution I found prevented the compiler from inferring the output type from the request type, it decoupled those two things too much. Obviously if I were to do a bunch of cursed internal unsafe rust I could probably make this work, but while I'm comfortable writing unsafe Rust I'm also conscious that I'm writing Jacquard not just for myself. My code will run in situations I cannot anticipate, and it needs to be as reliable as possible and as usable as possible. Additional use of unsafe could help with the latter (laundering lifetimes would make a number of things in Jacquard's main code paths much easier, both for me and for users of the library) but at potential cost to the former if I'm not smart enough or comprehensive enough in my testing. 253 + 254 + So I leave you, dear reader, with some questions this time. 255 + 256 + What choices make sense here? For Jacquard as a library, for writing web applications in Rust, and so on. I'm pretty damn good at this (if I do say so myself, and enough other people agree that I must accept it), but I'm also one person, with a necessarily incomplete understanding of the totality of the field.
weaver_notes/cargo_bloated.png

This is a binary file and will not be displayed.