working through somee editor bugs, etc

Orual b95a826f 6f2e5ec3

+2324 -144
+2
Cargo.lock
··· 11788 "jacquard-common", 11789 "jacquard-repo", 11790 "k256", 11791 "miette 7.6.0", 11792 "n0-future 0.1.3", 11793 "rand 0.8.5", 11794 "regex",
··· 11788 "jacquard-common", 11789 "jacquard-repo", 11790 "k256", 11791 + "loro", 11792 "miette 7.6.0", 11793 + "mini-moka 0.11.0", 11794 "n0-future 0.1.3", 11795 "rand 0.8.5", 11796 "regex",
+1 -1
Cargo.toml
··· 62 # [patch.crates-io] 63 # #secp256k1-zkp = { git = "https://github.com/dpc/rust-secp256k1-zkp/", branch = "sanket-pr" } 64 # ring = { git = "https://github.com/dpc/ring", rev = "5493e7e76d0d8fb1d3cbb0be9c4944700741b802" } 65 - 66 67 [profile] 68
··· 62 # [patch.crates-io] 63 # #secp256k1-zkp = { git = "https://github.com/dpc/rust-secp256k1-zkp/", branch = "sanket-pr" } 64 # ring = { git = "https://github.com/dpc/ring", rev = "5493e7e76d0d8fb1d3cbb0be9c4944700741b802" } 65 + loro = "1.9.1" 66 67 [profile] 68
+1 -1
build-workers.sh
··· 5 export RUSTFLAGS='--cfg getrandom_backend="wasm_js"' 6 cargo build -p weaver-app --bin editor_worker --bin embed_worker \ 7 --target wasm32-unknown-unknown --release \ 8 - --no-default-features --features "web","collab-worker" 9 10 echo "==> Running wasm-bindgen" 11 wasm-bindgen target/wasm32-unknown-unknown/release/editor_worker.wasm \
··· 5 export RUSTFLAGS='--cfg getrandom_backend="wasm_js"' 6 cargo build -p weaver-app --bin editor_worker --bin embed_worker \ 7 --target wasm32-unknown-unknown --release \ 8 + --no-default-features --features "web","collab-worker","use-index" 9 10 echo "==> Running wasm-bindgen" 11 wasm-bindgen target/wasm32-unknown-unknown/release/editor_worker.wasm \
+1 -1
crates/weaver-app/Dockerfile
··· 32 # Build wasm workers 33 RUN RUSTFLAGS='--cfg getrandom_backend="wasm_js"' cargo build -p weaver-app --bin editor_worker --bin embed_worker \ 34 --target wasm32-unknown-unknown --release \ 35 - --no-default-features --features "web, use-index" 36 37 # Run wasm-bindgen on workers 38 RUN wasm-bindgen target/wasm32-unknown-unknown/release/editor_worker.wasm \
··· 32 # Build wasm workers 33 RUN RUSTFLAGS='--cfg getrandom_backend="wasm_js"' cargo build -p weaver-app --bin editor_worker --bin embed_worker \ 34 --target wasm32-unknown-unknown --release \ 35 + --no-default-features --features "web","collab-worker","use-index" 36 37 # Run wasm-bindgen on workers 38 RUN wasm-bindgen target/wasm32-unknown-unknown/release/editor_worker.wasm \
+642 -8
crates/weaver-app/public/editor_worker.js
··· 17 ? { register: () => {}, unregister: () => {} } 18 : new FinalizationRegistry(state => state.dtor(state.a, state.b)); 19 20 function getArrayU8FromWasm0(ptr, len) { 21 ptr = ptr >>> 0; 22 return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); ··· 167 168 let WASM_VECTOR_LEN = 0; 169 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); 172 } 173 174 function wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___wasm_bindgen_1add006a0ed82fd3___JsValue_____(arg0, arg1, arg2) { 175 wasm.wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___wasm_bindgen_1add006a0ed82fd3___JsValue_____(arg0, arg1, arg2); 176 } 177 178 const JSOwnerFinalization = (typeof FinalizationRegistry === 'undefined') 179 ? { register: () => {}, unregister: () => {} } 180 : new FinalizationRegistry(ptr => wasm.__wbg_jsowner_free(ptr >>> 0, 1)); 181 182 class JSOwner { 183 __destroy_into_raw() { 184 const ptr = this.__wbg_ptr; ··· 229 function __wbg_get_imports() { 230 const imports = {}; 231 imports.wbg = {}; 232 imports.wbg.__wbg___wbindgen_is_function_8d400b8b1af978cd = function(arg0) { 233 const ret = typeof(arg0) === 'function'; 234 return ret; ··· 246 const ret = arg0 === undefined; 247 return ret; 248 }; 249 imports.wbg.__wbg___wbindgen_throw_dd24417ed36fc46e = function(arg0, arg1) { 250 throw new Error(getStringFromWasm0(arg0, arg1)); 251 }; 252 imports.wbg.__wbg__wbg_cb_unref_87dfb5aaa0cbcea7 = function(arg0) { 253 arg0._wbg_cb_unref(); 254 }; 255 imports.wbg.__wbg_call_3020136f7a2d6e44 = function() { return handleError(function (arg0, arg1, arg2) { 256 const ret = arg0.call(arg1, arg2); 257 return ret; ··· 260 const ret = arg0.call(arg1); 261 return ret; 262 }, arguments) }; 263 imports.wbg.__wbg_close_0b472ca2d13f54f7 = function(arg0) { 264 arg0.close(); 265 }; 266 imports.wbg.__wbg_crypto_574e78ad8b13b65f = function(arg0) { 267 const ret = arg0.crypto; 268 return ret; ··· 271 const ret = arg0.data; 272 return ret; 273 }; 274 imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function(arg0, arg1) { 275 let deferred0_0; 276 let deferred0_1; ··· 282 wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); 283 } 284 }; 285 imports.wbg.__wbg_getRandomValues_b8f5dbd5f3995a9e = function() { return handleError(function (arg0, arg1) { 286 arg0.getRandomValues(arg1); 287 }, arguments) }; 288 imports.wbg.__wbg_instanceof_Window_b5cf7783caa68180 = function(arg0) { 289 let result; 290 try { ··· 293 result = false; 294 } 295 const ret = result; 296 return ret; 297 }; 298 imports.wbg.__wbg_length_22ac23eaec9d8053 = function(arg0) { ··· 340 wasm.__wbindgen_free(deferred1_0, deferred1_1, 1); 341 } 342 }, arguments) }; 343 imports.wbg.__wbg_msCrypto_a61aeb35a24c1329 = function(arg0) { 344 const ret = arg0.msCrypto; 345 return ret; 346 }; 347 imports.wbg.__wbg_new_8a6f238a6ece86ea = function() { 348 const ret = new Error(); 349 return ret; 350 }; 351 imports.wbg.__wbg_new_from_slice_f9c22b9153b26992 = function(arg0, arg1) { 352 const ret = new Uint8Array(getArrayU8FromWasm0(arg0, arg1)); 353 return ret; ··· 356 const ret = new Function(getStringFromWasm0(arg0, arg1)); 357 return ret; 358 }; 359 imports.wbg.__wbg_new_with_length_aa5eaf41d35235e5 = function(arg0) { 360 const ret = new Uint8Array(arg0 >>> 0); 361 return ret; 362 }; 363 imports.wbg.__wbg_node_905d3e251edff8a2 = function(arg0) { 364 const ret = arg0.node; 365 return ret; 366 }; 367 imports.wbg.__wbg_now_8cf15d6e317793e1 = function(arg0) { ··· 372 const ret = Date.now(); 373 return ret; 374 }; 375 imports.wbg.__wbg_performance_c77a440eff2efd9b = function(arg0) { 376 const ret = arg0.performance; 377 return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); ··· 386 imports.wbg.__wbg_prototypesetcall_dfe9b766cdc1f1fd = function(arg0, arg1, arg2) { 387 Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2); 388 }; 389 imports.wbg.__wbg_queueMicrotask_9b549dfce8865860 = function(arg0) { 390 const ret = arg0.queueMicrotask; 391 return ret; ··· 396 imports.wbg.__wbg_randomFillSync_ac0988aba3254290 = function() { return handleError(function (arg0, arg1) { 397 arg0.randomFillSync(arg1); 398 }, arguments) }; 399 imports.wbg.__wbg_require_60cc747a6bc5215a = function() { return handleError(function () { 400 const ret = module.require; 401 return ret; ··· 404 const ret = Promise.resolve(arg0); 405 return ret; 406 }; 407 imports.wbg.__wbg_set_onmessage_5fe29d0fb54cb575 = function(arg0, arg1) { 408 arg0.onmessage = arg1; 409 }; 410 imports.wbg.__wbg_stack_0ed75d68575b0f3c = function(arg0, arg1) { 411 const ret = arg1.stack; 412 const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); ··· 430 const ret = typeof window === 'undefined' ? null : window; 431 return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); 432 }; 433 imports.wbg.__wbg_subarray_845f2f5bce7d061a = function(arg0, arg1, arg2) { 434 const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0); 435 return ret; 436 }; 437 imports.wbg.__wbg_then_4f95312d68691235 = function(arg0, arg1) { 438 const ret = arg0.then(arg1); 439 return ret; 440 }; 441 imports.wbg.__wbg_versions_c01dfd4722a88165 = function(arg0) { 442 const ret = arg0.versions; 443 return ret; 444 }; 445 imports.wbg.__wbindgen_cast_2241b6af4c4b2941 = function(arg0, arg1) { 446 // Cast intrinsic for `Ref(String) -> Externref`. 447 const ret = getStringFromWasm0(arg0, arg1); 448 return ret; 449 }; 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_____); 453 return ret; 454 }; 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_____); 458 return ret; 459 }; 460 imports.wbg.__wbindgen_cast_cb9088102bce6b30 = function(arg0, arg1) { 461 // Cast intrinsic for `Ref(Slice(U8)) -> NamedExternref("Uint8Array")`. 462 const ret = getArrayU8FromWasm0(arg0, arg1); 463 return ret; 464 }; 465 imports.wbg.__wbindgen_init_externref_table = function() {
··· 17 ? { register: () => {}, unregister: () => {} } 18 : new FinalizationRegistry(state => state.dtor(state.a, state.b)); 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 + 85 function getArrayU8FromWasm0(ptr, len) { 86 ptr = ptr >>> 0; 87 return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); ··· 232 233 let WASM_VECTOR_LEN = 0; 234 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); 245 } 246 247 function wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___wasm_bindgen_1add006a0ed82fd3___JsValue_____(arg0, arg1, arg2) { 248 wasm.wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___wasm_bindgen_1add006a0ed82fd3___JsValue_____(arg0, arg1, arg2); 249 } 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 + 293 const JSOwnerFinalization = (typeof FinalizationRegistry === 'undefined') 294 ? { register: () => {}, unregister: () => {} } 295 : new FinalizationRegistry(ptr => wasm.__wbg_jsowner_free(ptr >>> 0, 1)); 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 + 411 class JSOwner { 412 __destroy_into_raw() { 413 const ptr = this.__wbg_ptr; ··· 458 function __wbg_get_imports() { 459 const imports = {}; 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 + }; 473 imports.wbg.__wbg___wbindgen_is_function_8d400b8b1af978cd = function(arg0) { 474 const ret = typeof(arg0) === 'function'; 475 return ret; ··· 487 const ret = arg0 === undefined; 488 return ret; 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 + }; 498 imports.wbg.__wbg___wbindgen_throw_dd24417ed36fc46e = function(arg0, arg1) { 499 throw new Error(getStringFromWasm0(arg0, arg1)); 500 }; 501 imports.wbg.__wbg__wbg_cb_unref_87dfb5aaa0cbcea7 = function(arg0) { 502 arg0._wbg_cb_unref(); 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 + }; 540 imports.wbg.__wbg_call_3020136f7a2d6e44 = function() { return handleError(function (arg0, arg1, arg2) { 541 const ret = arg0.call(arg1, arg2); 542 return ret; ··· 545 const ret = arg0.call(arg1); 546 return ret; 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) }; 566 imports.wbg.__wbg_close_0b472ca2d13f54f7 = function(arg0) { 567 arg0.close(); 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 + }; 583 imports.wbg.__wbg_crypto_574e78ad8b13b65f = function(arg0) { 584 const ret = arg0.crypto; 585 return ret; ··· 588 const ret = arg0.data; 589 return ret; 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) }; 598 imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function(arg0, arg1) { 599 let deferred0_0; 600 let deferred0_1; ··· 606 wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); 607 } 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) }; 620 imports.wbg.__wbg_getRandomValues_b8f5dbd5f3995a9e = function() { return handleError(function (arg0, arg1) { 621 arg0.getRandomValues(arg1); 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 + }; 677 imports.wbg.__wbg_instanceof_Window_b5cf7783caa68180 = function(arg0) { 678 let result; 679 try { ··· 682 result = false; 683 } 684 const ret = result; 685 + return ret; 686 + }; 687 + imports.wbg.__wbg_iterator_27b7c8b35ab3e86b = function() { 688 + const ret = Symbol.iterator; 689 return ret; 690 }; 691 imports.wbg.__wbg_length_22ac23eaec9d8053 = function(arg0) { ··· 733 wasm.__wbindgen_free(deferred1_0, deferred1_1, 1); 734 } 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 + }; 743 imports.wbg.__wbg_msCrypto_a61aeb35a24c1329 = function(arg0) { 744 const ret = arg0.msCrypto; 745 return ret; 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) }; 771 imports.wbg.__wbg_new_8a6f238a6ece86ea = function() { 772 const ret = new Error(); 773 return ret; 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 + }; 797 imports.wbg.__wbg_new_from_slice_f9c22b9153b26992 = function(arg0, arg1) { 798 const ret = new Uint8Array(getArrayU8FromWasm0(arg0, arg1)); 799 return ret; ··· 802 const ret = new Function(getStringFromWasm0(arg0, arg1)); 803 return ret; 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 + }; 809 imports.wbg.__wbg_new_with_length_aa5eaf41d35235e5 = function(arg0) { 810 const ret = new Uint8Array(arg0 >>> 0); 811 return ret; 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) }; 829 imports.wbg.__wbg_node_905d3e251edff8a2 = function(arg0) { 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(); 839 return ret; 840 }; 841 imports.wbg.__wbg_now_8cf15d6e317793e1 = function(arg0) { ··· 846 const ret = Date.now(); 847 return ret; 848 }; 849 + imports.wbg.__wbg_performance_7a3ffd0b17f663ad = function(arg0) { 850 + const ret = arg0.performance; 851 + return ret; 852 + }; 853 imports.wbg.__wbg_performance_c77a440eff2efd9b = function(arg0) { 854 const ret = arg0.performance; 855 return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); ··· 864 imports.wbg.__wbg_prototypesetcall_dfe9b766cdc1f1fd = function(arg0, arg1, arg2) { 865 Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2); 866 }; 867 + imports.wbg.__wbg_push_7d9be8f38fc13975 = function(arg0, arg1) { 868 + const ret = arg0.push(arg1); 869 + return ret; 870 + }; 871 imports.wbg.__wbg_queueMicrotask_9b549dfce8865860 = function(arg0) { 872 const ret = arg0.queueMicrotask; 873 return ret; ··· 878 imports.wbg.__wbg_randomFillSync_ac0988aba3254290 = function() { return handleError(function (arg0, arg1) { 879 arg0.randomFillSync(arg1); 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) }; 902 imports.wbg.__wbg_require_60cc747a6bc5215a = function() { return handleError(function () { 903 const ret = module.require; 904 return ret; ··· 907 const ret = Promise.resolve(arg0); 908 return ret; 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 + }; 960 imports.wbg.__wbg_set_onmessage_5fe29d0fb54cb575 = function(arg0, arg1) { 961 arg0.onmessage = arg1; 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 + }; 976 imports.wbg.__wbg_stack_0ed75d68575b0f3c = function(arg0, arg1) { 977 const ret = arg1.stack; 978 const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); ··· 996 const ret = typeof window === 'undefined' ? null : window; 997 return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); 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) }; 1007 imports.wbg.__wbg_subarray_845f2f5bce7d061a = function(arg0, arg1, arg2) { 1008 const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0); 1009 return ret; 1010 }; 1011 + imports.wbg.__wbg_then_429f7caf1026411d = function(arg0, arg1, arg2) { 1012 + const ret = arg0.then(arg1, arg2); 1013 + return ret; 1014 + }; 1015 imports.wbg.__wbg_then_4f95312d68691235 = function(arg0, arg1) { 1016 const ret = arg0.then(arg1); 1017 return ret; 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 + }; 1037 imports.wbg.__wbg_versions_c01dfd4722a88165 = function(arg0) { 1038 const ret = arg0.versions; 1039 return ret; 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 + }; 1049 imports.wbg.__wbindgen_cast_2241b6af4c4b2941 = function(arg0, arg1) { 1050 // Cast intrinsic for `Ref(String) -> Externref`. 1051 const ret = getStringFromWasm0(arg0, arg1); 1052 return ret; 1053 }; 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_); 1057 return ret; 1058 }; 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_____); 1067 return ret; 1068 }; 1069 imports.wbg.__wbindgen_cast_cb9088102bce6b30 = function(arg0, arg1) { 1070 // Cast intrinsic for `Ref(Slice(U8)) -> NamedExternref("Uint8Array")`. 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_____); 1097 return ret; 1098 }; 1099 imports.wbg.__wbindgen_init_externref_table = function() {
+217 -28
crates/weaver-app/public/embed_worker.js
··· 232 233 let WASM_VECTOR_LEN = 0; 234 235 - function wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke_______1_(arg0, arg1) { 236 - wasm.wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke_______1_(arg0, arg1); 237 } 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); 241 } 242 243 function wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke______(arg0, arg1) { ··· 248 wasm.wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___wasm_bindgen_1add006a0ed82fd3___JsValue_____(arg0, arg1, arg2); 249 } 250 251 const __wbindgen_enum_RequestCache = ["default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached"]; 252 253 const __wbindgen_enum_RequestCredentials = ["omit", "same-origin", "include"]; 254 255 const __wbindgen_enum_RequestMode = ["same-origin", "no-cors", "cors", "navigate"]; 256 257 const JSOwnerFinalization = (typeof FinalizationRegistry === 'undefined') 258 ? { register: () => {}, unregister: () => {} } 259 : new FinalizationRegistry(ptr => wasm.__wbg_jsowner_free(ptr >>> 0, 1)); 260 261 class JSOwner { 262 __destroy_into_raw() { 263 const ptr = this.__wbg_ptr; ··· 355 const ret = arg0.arrayBuffer(); 356 return ret; 357 }, arguments) }; 358 imports.wbg.__wbg_call_abb4ff46ce38be40 = function() { return handleError(function (arg0, arg1) { 359 const ret = arg0.call(arg1); 360 return ret; ··· 362 imports.wbg.__wbg_clearTimeout_15dfc3d1dcb635c6 = function() { return handleError(function (arg0, arg1) { 363 arg0.clearTimeout(arg1); 364 }, arguments) }; 365 - imports.wbg.__wbg_clearTimeout_3b6a9a13d4bde075 = function(arg0) { 366 const ret = clearTimeout(arg0); 367 return ret; 368 }; 369 imports.wbg.__wbg_close_0b472ca2d13f54f7 = function(arg0) { 370 arg0.close(); 371 }; 372 imports.wbg.__wbg_data_8bf4ae669a78a688 = function(arg0) { 373 const ret = arg0.data; 374 return ret; ··· 377 const ret = arg0.done; 378 return ret; 379 }; 380 imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function(arg0, arg1) { 381 let deferred0_0; 382 let deferred0_1; ··· 388 wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); 389 } 390 }; 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) { 396 const ret = fetch(arg0); 397 return ret; 398 }; 399 - imports.wbg.__wbg_getTime_ad1e9878a735af08 = function(arg0) { 400 - const ret = arg0.getTime(); 401 return ret; 402 }; 403 imports.wbg.__wbg_get_af9dab7e9603ea93 = function() { return handleError(function (arg0, arg1) { ··· 440 const ret = arg0.length; 441 return ret; 442 }; 443 - imports.wbg.__wbg_new_0_23cedd11d9b40c9d = function() { 444 - const ret = new Date(); 445 - return ret; 446 - }; 447 imports.wbg.__wbg_new_1ba21ce319a06297 = function() { 448 const ret = new Object(); 449 return ret; ··· 464 const ret = new Error(); 465 return ret; 466 }; 467 imports.wbg.__wbg_new_from_slice_f9c22b9153b26992 = function(arg0, arg1) { 468 const ret = new Uint8Array(getArrayU8FromWasm0(arg0, arg1)); 469 return ret; 470 }; 471 imports.wbg.__wbg_new_no_args_cb138f77cf6151ee = function(arg0, arg1) { 472 const ret = new Function(getStringFromWasm0(arg0, arg1)); 473 return ret; 474 }; 475 imports.wbg.__wbg_new_with_str_and_init_c5748f76f5108934 = function() { return handleError(function (arg0, arg1, arg2) { ··· 517 const ret = Promise.resolve(arg0); 518 return ret; 519 }; 520 - imports.wbg.__wbg_setTimeout_35a07631c669fbee = function(arg0, arg1) { 521 const ret = setTimeout(arg0, arg1); 522 return ret; 523 }; ··· 525 const ret = arg0.setTimeout(arg1, arg2); 526 return ret; 527 }, arguments) }; 528 imports.wbg.__wbg_set_body_8e743242d6076a4f = function(arg0, arg1) { 529 arg0.body = arg1; 530 }; ··· 607 const ret = arg0.value; 608 return ret; 609 }; 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; 614 }; 615 imports.wbg.__wbindgen_cast_2241b6af4c4b2941 = function(arg0, arg1) { 616 // Cast intrinsic for `Ref(String) -> Externref`. 617 const ret = getStringFromWasm0(arg0, arg1); 618 return ret; 619 }; 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______); 623 return ret; 624 }; 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`. 627 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 return ret; 629 }; 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`. 632 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 return ret; 634 };
··· 232 233 let WASM_VECTOR_LEN = 0; 234 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 } 238 239 + function wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke_______1_(arg0, arg1) { 240 + wasm.wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke_______1_(arg0, arg1); 241 } 242 243 function wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke______(arg0, arg1) { ··· 248 wasm.wasm_bindgen_1add006a0ed82fd3___convert__closures_____invoke___wasm_bindgen_1add006a0ed82fd3___JsValue_____(arg0, arg1, arg2); 249 } 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 + 257 const __wbindgen_enum_RequestCache = ["default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached"]; 258 259 const __wbindgen_enum_RequestCredentials = ["omit", "same-origin", "include"]; 260 261 const __wbindgen_enum_RequestMode = ["same-origin", "no-cors", "cors", "navigate"]; 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 + 275 const JSOwnerFinalization = (typeof FinalizationRegistry === 'undefined') 276 ? { register: () => {}, unregister: () => {} } 277 : new FinalizationRegistry(ptr => wasm.__wbg_jsowner_free(ptr >>> 0, 1)); 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 + 393 class JSOwner { 394 __destroy_into_raw() { 395 const ptr = this.__wbg_ptr; ··· 487 const ret = arg0.arrayBuffer(); 488 return ret; 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) }; 510 imports.wbg.__wbg_call_abb4ff46ce38be40 = function() { return handleError(function (arg0, arg1) { 511 const ret = arg0.call(arg1); 512 return ret; ··· 514 imports.wbg.__wbg_clearTimeout_15dfc3d1dcb635c6 = function() { return handleError(function (arg0, arg1) { 515 arg0.clearTimeout(arg1); 516 }, arguments) }; 517 + imports.wbg.__wbg_clearTimeout_b716ecb44bea14ed = function(arg0) { 518 const ret = clearTimeout(arg0); 519 return ret; 520 }; 521 + imports.wbg.__wbg_close_0af5661bf3d335f2 = function() { return handleError(function (arg0) { 522 + arg0.close(); 523 + }, arguments) }; 524 imports.wbg.__wbg_close_0b472ca2d13f54f7 = function(arg0) { 525 arg0.close(); 526 }; 527 + imports.wbg.__wbg_close_3ec111e7b23d94d8 = function() { return handleError(function (arg0) { 528 + arg0.close(); 529 + }, arguments) }; 530 imports.wbg.__wbg_data_8bf4ae669a78a688 = function(arg0) { 531 const ret = arg0.data; 532 return ret; ··· 535 const ret = arg0.done; 536 return ret; 537 }; 538 + imports.wbg.__wbg_enqueue_a7e6b1ee87963aad = function() { return handleError(function (arg0, arg1) { 539 + arg0.enqueue(arg1); 540 + }, arguments) }; 541 imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function(arg0, arg1) { 542 let deferred0_0; 543 let deferred0_1; ··· 549 wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); 550 } 551 }; 552 + imports.wbg.__wbg_fetch_7fb7602a1bf647ec = function(arg0) { 553 const ret = fetch(arg0); 554 return ret; 555 }; 556 + imports.wbg.__wbg_fetch_90447c28cc0b095e = function(arg0, arg1) { 557 + const ret = arg0.fetch(arg1); 558 return ret; 559 }; 560 imports.wbg.__wbg_get_af9dab7e9603ea93 = function() { return handleError(function (arg0, arg1) { ··· 597 const ret = arg0.length; 598 return ret; 599 }; 600 imports.wbg.__wbg_new_1ba21ce319a06297 = function() { 601 const ret = new Object(); 602 return ret; ··· 617 const ret = new Error(); 618 return ret; 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 + }; 642 imports.wbg.__wbg_new_from_slice_f9c22b9153b26992 = function(arg0, arg1) { 643 const ret = new Uint8Array(getArrayU8FromWasm0(arg0, arg1)); 644 return ret; 645 }; 646 imports.wbg.__wbg_new_no_args_cb138f77cf6151ee = function(arg0, arg1) { 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); 652 return ret; 653 }; 654 imports.wbg.__wbg_new_with_str_and_init_c5748f76f5108934 = function() { return handleError(function (arg0, arg1, arg2) { ··· 696 const ret = Promise.resolve(arg0); 697 return ret; 698 }; 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) { 703 const ret = setTimeout(arg0, arg1); 704 return ret; 705 }; ··· 707 const ret = arg0.setTimeout(arg1, arg2); 708 return ret; 709 }, arguments) }; 710 + imports.wbg.__wbg_set_169e13b608078b7b = function(arg0, arg1, arg2) { 711 + arg0.set(getArrayU8FromWasm0(arg1, arg2)); 712 + }; 713 imports.wbg.__wbg_set_body_8e743242d6076a4f = function(arg0, arg1) { 714 arg0.body = arg1; 715 }; ··· 792 const ret = arg0.value; 793 return ret; 794 }; 795 + imports.wbg.__wbg_view_788aaf149deefd2f = function(arg0) { 796 + const ret = arg0.view; 797 + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); 798 }; 799 imports.wbg.__wbindgen_cast_2241b6af4c4b2941 = function(arg0, arg1) { 800 // Cast intrinsic for `Ref(String) -> Externref`. 801 const ret = getStringFromWasm0(arg0, arg1); 802 return ret; 803 }; 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_____); 807 return ret; 808 }; 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`. 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_____); 812 return ret; 813 }; 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`. 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_); 822 return ret; 823 };
+32 -1
crates/weaver-app/src/components/editor/component.rs
··· 93 let draft_key = draft_key.clone(); 94 let entry_uri = parsed_uri.clone(); 95 let initial_content = initial_content.clone(); 96 97 async move { 98 match load_and_merge_document(&fetcher, &draft_key, entry_uri.as_ref()).await { 99 - Ok(Some(state)) => { 100 tracing::debug!("Loaded merged document state"); 101 return LoadResult::Loaded(state); 102 } 103 Ok(None) => { ··· 302 synced_version: None, // Fresh from entry, never synced 303 last_seen_diffs: std::collections::HashMap::new(), 304 resolved_content, 305 }); 306 } 307 Err(e) => { ··· 327 synced_version: None, // New doc, never synced 328 last_seen_diffs: std::collections::HashMap::new(), 329 resolved_content: weaver_common::ResolvedContent::default(), 330 }) 331 } 332 Err(e) => { ··· 732 cursor_offset, 733 editing_uri, 734 editing_cid, 735 export_ms, 736 encode_ms, 737 } => { ··· 744 cursor_offset, 745 editing_uri, 746 editing_cid, 747 }; 748 let write_start = crate::perf::now(); 749 let _ = gloo_storage::LocalStorage::set( ··· 848 let cursor_offset = doc.cursor.read().offset; 849 let editing_uri = doc.entry_ref().map(|r| r.uri.to_smolstr()); 850 let editing_cid = doc.entry_ref().map(|r| r.cid.to_smolstr()); 851 852 let sink_clone = worker_sink.clone(); 853 ··· 865 cursor_offset, 866 editing_uri, 867 editing_cid, 868 }) 869 .await; 870 }
··· 93 let draft_key = draft_key.clone(); 94 let entry_uri = parsed_uri.clone(); 95 let initial_content = initial_content.clone(); 96 + let target_notebook = target_notebook.clone(); 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 + 119 match load_and_merge_document(&fetcher, &draft_key, entry_uri.as_ref()).await { 120 + Ok(Some(mut state)) => { 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 + } 126 return LoadResult::Loaded(state); 127 } 128 Ok(None) => { ··· 327 synced_version: None, // Fresh from entry, never synced 328 last_seen_diffs: std::collections::HashMap::new(), 329 resolved_content, 330 + notebook_uri: notebook_uri.clone(), 331 }); 332 } 333 Err(e) => { ··· 353 synced_version: None, // New doc, never synced 354 last_seen_diffs: std::collections::HashMap::new(), 355 resolved_content: weaver_common::ResolvedContent::default(), 356 + notebook_uri, 357 }) 358 } 359 Err(e) => { ··· 759 cursor_offset, 760 editing_uri, 761 editing_cid, 762 + notebook_uri, 763 export_ms, 764 encode_ms, 765 } => { ··· 772 cursor_offset, 773 editing_uri, 774 editing_cid, 775 + notebook_uri, 776 }; 777 let write_start = crate::perf::now(); 778 let _ = gloo_storage::LocalStorage::set( ··· 877 let cursor_offset = doc.cursor.read().offset; 878 let editing_uri = doc.entry_ref().map(|r| r.uri.to_smolstr()); 879 let editing_cid = doc.entry_ref().map(|r| r.cid.to_smolstr()); 880 + let notebook_uri = doc.notebook_uri(); 881 882 let sink_clone = worker_sink.clone(); 883 ··· 895 cursor_offset, 896 editing_uri, 897 editing_cid, 898 + notebook_uri, 899 }) 900 .await; 901 }
+19
crates/weaver-app/src/components/editor/document.rs
··· 23 24 use jacquard::IntoStatic; 25 use jacquard::from_json_value; 26 use jacquard::types::string::AtUri; 27 use weaver_api::com_atproto::repo::strong_ref::StrongRef; 28 use weaver_api::sh_weaver::embed::images::Image; ··· 90 /// None for new entries that haven't been published yet. 91 /// Signal so cloned docs share the same state after publish. 92 pub entry_ref: Signal<Option<StrongRef<'static>>>, 93 94 // --- Edit sync state (for PDS sync) --- 95 /// StrongRef to the sh.weaver.edit.root record for this edit session. ··· 235 /// Pre-resolved embed content fetched during load. 236 /// Avoids embed pop-in on initial render. 237 pub resolved_content: weaver_common::ResolvedContent, 238 } 239 240 impl PartialEq for LoadedDocState { ··· 316 tags, 317 embeds, 318 entry_ref: Signal::new(None), 319 edit_root: Signal::new(None), 320 last_diff: Signal::new(None), 321 last_synced_version: Signal::new(None), ··· 490 /// Set the StrongRef when editing an existing entry. 491 pub fn set_entry_ref(&mut self, entry: Option<StrongRef<'static>>) { 492 self.entry_ref.set(entry); 493 } 494 495 // --- Tags accessors --- ··· 1090 tags, 1091 embeds, 1092 entry_ref: Signal::new(None), 1093 edit_root: Signal::new(None), 1094 last_diff: Signal::new(None), 1095 last_synced_version: Signal::new(None), ··· 1148 tags, 1149 embeds, 1150 entry_ref: Signal::new(state.entry_ref), 1151 edit_root: Signal::new(state.edit_root), 1152 last_diff: Signal::new(state.last_diff), 1153 // Use the synced version from state (tracks the PDS version vector)
··· 23 24 use jacquard::IntoStatic; 25 use jacquard::from_json_value; 26 + use jacquard::smol_str::SmolStr; 27 use jacquard::types::string::AtUri; 28 use weaver_api::com_atproto::repo::strong_ref::StrongRef; 29 use weaver_api::sh_weaver::embed::images::Image; ··· 91 /// None for new entries that haven't been published yet. 92 /// Signal so cloned docs share the same state after publish. 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>>, 97 98 // --- Edit sync state (for PDS sync) --- 99 /// StrongRef to the sh.weaver.edit.root record for this edit session. ··· 239 /// Pre-resolved embed content fetched during load. 240 /// Avoids embed pop-in on initial render. 241 pub resolved_content: weaver_common::ResolvedContent, 242 + /// Notebook URI for re-publishing to the same notebook. 243 + pub notebook_uri: Option<SmolStr>, 244 } 245 246 impl PartialEq for LoadedDocState { ··· 322 tags, 323 embeds, 324 entry_ref: Signal::new(None), 325 + notebook_uri: Signal::new(None), 326 edit_root: Signal::new(None), 327 last_diff: Signal::new(None), 328 last_synced_version: Signal::new(None), ··· 497 /// Set the StrongRef when editing an existing entry. 498 pub fn set_entry_ref(&mut self, entry: Option<StrongRef<'static>>) { 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); 510 } 511 512 // --- Tags accessors --- ··· 1107 tags, 1108 embeds, 1109 entry_ref: Signal::new(None), 1110 + notebook_uri: Signal::new(None), 1111 edit_root: Signal::new(None), 1112 last_diff: Signal::new(None), 1113 last_synced_version: Signal::new(None), ··· 1166 tags, 1167 embeds, 1168 entry_ref: Signal::new(state.entry_ref), 1169 + notebook_uri: Signal::new(state.notebook_uri), 1170 edit_root: Signal::new(state.edit_root), 1171 last_diff: Signal::new(state.last_diff), 1172 // Use the synced version from state (tracks the PDS version vector)
+2 -2
crates/weaver-app/src/components/editor/mod.rs
··· 58 // Storage 59 #[allow(unused_imports)] 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, 63 }; 64 65 // Sync
··· 58 // Storage 59 #[allow(unused_imports)] 60 pub use 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 }; 64 65 // Sync
+42 -8
crates/weaver-app/src/components/editor/publish.rs
··· 4 5 use dioxus::prelude::*; 6 use jacquard::cowstr::ToCowStr; 7 use jacquard::types::collection::Collection; 8 use jacquard::types::ident::AtIdentifier; 9 use jacquard::types::recordkey::RecordKey; ··· 252 .maybe_embeds(entry_embeds) 253 .build(); 254 255 // Pass existing rkey if re-publishing (to allow title changes without creating new entry) 256 let doc_entry_ref = doc.entry_ref(); 257 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, 261 &doc.title(), 262 entry, 263 existing_rkey.map(|r| r.0.as_str()), ··· 268 // Set entry_ref so subsequent publishes update this record 269 doc.set_entry_ref(Some(entry_ref)); 270 271 if was_created { 272 PublishResult::Created(uri) 273 } else { ··· 288 // Check if we're the owner or a collaborator 289 let owner_did = match existing_ref.uri.authority() { 290 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)))?, 296 }; 297 let is_collaborator = owner_did != current_did; 298 ··· 304 .title(doc.title()) 305 .path(path) 306 .created_at(Datetime::now()) 307 .maybe_tags(tags) 308 .maybe_embeds(entry_embeds) 309 .build();
··· 4 5 use dioxus::prelude::*; 6 use jacquard::cowstr::ToCowStr; 7 + use jacquard::smol_str::ToSmolStr; 8 use jacquard::types::collection::Collection; 9 use jacquard::types::ident::AtIdentifier; 10 use jacquard::types::recordkey::RecordKey; ··· 253 .maybe_embeds(entry_embeds) 254 .build(); 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 + 284 // Pass existing rkey if re-publishing (to allow title changes without creating new entry) 285 let doc_entry_ref = doc.entry_ref(); 286 let existing_rkey = doc_entry_ref.as_ref().and_then(|r| r.uri.rkey()); 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, 293 &doc.title(), 294 entry, 295 existing_rkey.map(|r| r.0.as_str()), ··· 300 // Set entry_ref so subsequent publishes update this record 301 doc.set_entry_ref(Some(entry_ref)); 302 303 + // Store the notebook URI for future re-publishing 304 + doc.set_notebook_uri(Some(notebook_uri_final.to_smolstr())); 305 + 306 if was_created { 307 PublishResult::Created(uri) 308 } else { ··· 323 // Check if we're the owner or a collaborator 324 let owner_did = match existing_ref.uri.authority() { 325 AtIdentifier::Did(d) => d.clone(), 326 + AtIdentifier::Handle(h) => fetcher.client.resolve_handle(h).await.map_err(|e| { 327 + WeaverError::InvalidNotebook(format!("Failed to resolve handle: {}", e)) 328 + })?, 329 }; 330 let is_collaborator = owner_did != current_did; 331 ··· 337 .title(doc.title()) 338 .path(path) 339 .created_at(Datetime::now()) 340 + .updated_at(Datetime::now()) 341 .maybe_tags(tags) 342 .maybe_embeds(entry_embeds) 343 .build();
+80 -1
crates/weaver-app/src/components/editor/storage.rs
··· 22 use jacquard::IntoStatic; 23 use jacquard::smol_str::{SmolStr, ToSmolStr}; 24 use jacquard::types::string::{AtUri, Cid}; 25 - use weaver_api::com_atproto::repo::strong_ref::StrongRef; 26 use loro::cursor::Cursor; 27 use serde::{Deserialize, Serialize}; 28 29 use super::document::EditorDocument; 30 ··· 71 /// CID of the entry if editing an existing entry 72 #[serde(default, skip_serializing_if = "Option::is_none")] 73 pub editing_cid: Option<SmolStr>, 74 } 75 76 /// Build the full storage key from a draft key. ··· 104 cursor_offset: doc.cursor.read().offset, 105 editing_uri: doc.entry_ref().map(|r| r.uri.to_smolstr()), 106 editing_cid: doc.entry_ref().map(|r| r.cid.to_smolstr()), 107 }; 108 109 let write_start = crate::perf::now(); ··· 151 // Verify the content matches (sanity check) 152 if doc.content() == snapshot.content { 153 doc.set_entry_ref(entry_ref.clone()); 154 return Some(doc); 155 } 156 tracing::warn!("Snapshot content mismatch, falling back to text content"); ··· 162 doc.cursor.write().offset = snapshot.cursor_offset.min(doc.len_chars()); 163 doc.sync_loro_cursor(); 164 doc.set_entry_ref(entry_ref); 165 Some(doc) 166 } 167 ··· 171 pub snapshot: Vec<u8>, 172 /// Entry StrongRef if editing an existing entry 173 pub entry_ref: Option<StrongRef<'static>>, 174 } 175 176 /// Load snapshot data from LocalStorage (WASM only). ··· 201 Some(LocalSnapshotData { 202 snapshot: snapshot_bytes, 203 entry_ref, 204 }) 205 } 206 ··· 248 } 249 250 drafts 251 } 252 253 /// Clear all editor drafts from LocalStorage (WASM only).
··· 22 use jacquard::IntoStatic; 23 use jacquard::smol_str::{SmolStr, ToSmolStr}; 24 use jacquard::types::string::{AtUri, Cid}; 25 use loro::cursor::Cursor; 26 use serde::{Deserialize, Serialize}; 27 + use weaver_api::com_atproto::repo::strong_ref::StrongRef; 28 29 use super::document::EditorDocument; 30 ··· 71 /// CID of the entry if editing an existing entry 72 #[serde(default, skip_serializing_if = "Option::is_none")] 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>, 78 } 79 80 /// Build the full storage key from a draft key. ··· 108 cursor_offset: doc.cursor.read().offset, 109 editing_uri: doc.entry_ref().map(|r| r.uri.to_smolstr()), 110 editing_cid: doc.entry_ref().map(|r| r.cid.to_smolstr()), 111 + notebook_uri: doc.notebook_uri(), 112 }; 113 114 let write_start = crate::perf::now(); ··· 156 // Verify the content matches (sanity check) 157 if doc.content() == snapshot.content { 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 + } 162 return Some(doc); 163 } 164 tracing::warn!("Snapshot content mismatch, falling back to text content"); ··· 170 doc.cursor.write().offset = snapshot.cursor_offset.min(doc.len_chars()); 171 doc.sync_loro_cursor(); 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 + } 176 Some(doc) 177 } 178 ··· 182 pub snapshot: Vec<u8>, 183 /// Entry StrongRef if editing an existing entry 184 pub entry_ref: Option<StrongRef<'static>>, 185 + /// Notebook URI for re-publishing 186 + pub notebook_uri: Option<SmolStr>, 187 } 188 189 /// Load snapshot data from LocalStorage (WASM only). ··· 214 Some(LocalSnapshotData { 215 snapshot: snapshot_bytes, 216 entry_ref, 217 + notebook_uri: snapshot.notebook_uri, 218 }) 219 } 220 ··· 262 } 263 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(()) 330 } 331 332 /// Clear all editor drafts from LocalStorage (WASM only).
+242 -44
crates/weaver-app/src/components/editor/sync.rs
··· 239 } 240 } 241 242 /// Result of a sync operation. 243 #[derive(Clone, Debug)] 244 pub enum SyncResult { ··· 629 Ok(all_diffs) 630 } 631 632 /// Create the edit root record for an entry. 633 /// 634 /// Uploads the current Loro snapshot as a blob and creates an `sh.weaver.edit.root` 635 /// record referencing the entry (or draft key if unpublished). 636 /// 637 /// For drafts, also creates the `sh.weaver.edit.draft` stub record first. 638 pub async fn create_edit_root( 639 fetcher: &Fetcher, 640 doc: &EditorDocument, 641 draft_key: &str, 642 entry_uri: Option<&AtUri<'_>>, 643 entry_cid: Option<&Cid<'_>>, 644 - ) -> Result<(AtUri<'static>, Cid<'static>), WeaverError> { 645 let client = fetcher.get_client(); 646 let did = fetcher 647 .current_did() ··· 649 .ok_or_else(|| WeaverError::InvalidNotebook("Not authenticated".into()))?; 650 651 // For drafts, create the stub record first (makes it discoverable via listRecords) 652 - if entry_uri.is_none() { 653 let rkey = extract_draft_rkey(draft_key); 654 - // Try to create draft stub, ignore if it already exists 655 match create_draft_stub(fetcher, &did, &rkey).await { 656 - Ok((uri, _cid)) => { 657 tracing::debug!("Created draft stub: {}", uri); 658 } 659 Err(e) => { 660 - // Check if it's a "record already exists" error - that's fine 661 let err_str = e.to_string(); 662 - if !err_str.contains("RecordAlreadyExists") && !err_str.contains("already exists") { 663 tracing::warn!("Failed to create draft stub (continuing anyway): {}", e); 664 } 665 } 666 } 667 - } 668 669 // Export full snapshot 670 let snapshot = doc.export_snapshot(); ··· 708 .into_output() 709 .map_err(|e| WeaverError::InvalidNotebook(e.to_string()))?; 710 711 - Ok((output.uri.into_static(), output.cid.into_static())) 712 } 713 714 /// Create a diff record with updates since the last sync. ··· 827 if doc.edit_root().is_none() { 828 // First sync - create root 829 let create_start = crate::perf::now(); 830 - let (root_uri, root_cid) = create_edit_root( 831 fetcher, 832 doc, 833 draft_key, ··· 839 840 // Build StrongRef for the root 841 let root_ref = StrongRef::new() 842 - .uri(root_uri.clone()) 843 - .cid(root_cid.clone()) 844 .build(); 845 846 // Update document state ··· 848 doc.set_last_diff(None); 849 doc.mark_synced(); 850 851 let total_ms = crate::perf::now() - fn_start; 852 tracing::debug!(total_ms, create_ms, "sync_to_pds: created root"); 853 854 Ok(SyncResult::CreatedRoot { 855 - uri: root_uri, 856 - cid: root_cid, 857 }) 858 } else { 859 // Subsequent sync - create diff ··· 916 /// Last seen diff URI per collaborator root (for incremental sync). 917 /// Maps root URI -> last diff URI we've imported from that root. 918 pub last_seen_diffs: std::collections::HashMap<AtUri<'static>, AtUri<'static>>, 919 } 920 921 /// Fetch a blob from the PDS. ··· 1006 let merged_doc = LoroDoc::new(); 1007 let mut our_root_ref: Option<StrongRef<'static>> = None; 1008 let mut our_last_diff_ref: Option<StrongRef<'static>> = None; 1009 let mut updated_last_seen = last_seen_diffs.clone(); 1010 1011 // Get current user's DID to identify "our" root for sync state tracking ··· 1054 updated_last_seen.insert(uri.clone(), last_diff.uri.clone().into_static()); 1055 } 1056 1057 // Track "our" root/diff refs for sync state (used when syncing back) 1058 // We want to track our own edit.root so subsequent diffs go to the right place 1059 let is_our_root = current_did.as_ref().is_some_and(|did| root_did == *did); ··· 1089 root_snapshot: merged_snapshot.into(), 1090 diff_updates: vec![], // Already merged into snapshot 1091 last_seen_diffs: updated_last_seen, 1092 })) 1093 } 1094 ··· 1130 .uri(root_uri.clone()) 1131 .cid(root_cid.into_static()) 1132 .build(); 1133 1134 // Fetch the root snapshot blob 1135 let root_snapshot = fetch_blob( ··· 1149 root_snapshot, 1150 diff_updates: vec![], 1151 last_seen_diffs: std::collections::HashMap::new(), 1152 })); 1153 } 1154 ··· 1170 } 1171 } 1172 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(); 1176 1177 let diff_response = fetcher 1178 .client ··· 1226 root_snapshot, 1227 diff_updates, 1228 last_seen_diffs: std::collections::HashMap::new(), 1229 })) 1230 } 1231 ··· 1291 synced_version: None, // Local-only, never synced to PDS 1292 last_seen_diffs: std::collections::HashMap::new(), 1293 resolved_content, 1294 })) 1295 } 1296 ··· 1314 // Capture the version after loading all PDS state - this is our sync baseline 1315 let synced_version = Some(doc.oplog_vv()); 1316 1317 let resolved_content = 1318 prefetch_embeds_from_doc(&doc, fetcher, owner_ident.as_deref()).await; 1319 1320 Ok(Some(LoadedDocState { 1321 doc, 1322 - entry_ref: None, // Entry ref comes from the entry itself, not edit state 1323 edit_root: Some(pds.root_ref), 1324 last_diff: pds.last_diff_ref, 1325 synced_version, // Just loaded from PDS, fully synced 1326 last_seen_diffs: pds.last_seen_diffs, 1327 resolved_content, 1328 })) 1329 } 1330 ··· 1377 synced_version: Some(pds_version), 1378 last_seen_diffs: pds.last_seen_diffs, 1379 resolved_content, 1380 })) 1381 } 1382 } ··· 1413 pub document: EditorDocument, 1414 /// Draft key for this document 1415 pub draft_key: String, 1416 - /// Auto-sync interval in milliseconds (0 to disable) 1417 - #[props(default = 30_000)] 1418 pub auto_sync_interval_ms: u32, 1419 /// Callback to refresh/reload document from collaborators 1420 #[props(default)] ··· 1427 /// Sync status indicator with auto-sync functionality. 1428 /// 1429 /// Displays the current sync state and automatically syncs to PDS periodically. 1430 #[component] 1431 pub fn SyncStatus(props: SyncStatusProps) -> Element { 1432 let fetcher = use_context::<Fetcher>(); 1433 let auth_state = use_context::<Signal<AuthState>>(); 1434 1435 // Sync state management 1436 let mut sync_state = use_signal(|| { 1437 if props.document.has_unsynced_changes() { ··· 1442 }); 1443 let mut last_error: Signal<Option<String>> = use_signal(|| None); 1444 1445 - let doc = props.document.clone(); 1446 - let draft_key = props.draft_key.clone(); 1447 - 1448 // Check if we're authenticated (drafts can sync via DraftRef even without entry) 1449 let is_authenticated = auth_state.read().is_authenticated(); 1450 1451 // Auto-sync trigger signal - set to true to trigger a sync 1452 let mut trigger_sync = use_signal(|| false); 1453 1454 - // Auto-sync timer - triggers sync when there are unsynced changes 1455 { 1456 - let auto_sync_interval_ms = props.auto_sync_interval_ms; 1457 let doc_for_check = doc.clone(); 1458 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 - ); 1471 } 1472 1473 // Collaborator poll timer - checks for collaborator updates periodically ··· 1572 Ok(_) => { 1573 sync_state.set(SyncState::Synced); 1574 last_error.set(None); 1575 tracing::debug!("Sync completed successfully"); 1576 } 1577 Err(e) => { ··· 1584 }); 1585 1586 // Determine display state (drafts can sync too via DraftRef) 1587 let display_state = if !is_authenticated { 1588 SyncState::Disabled 1589 } else { 1590 *sync_state.read() 1591 }; 1592 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"), 1600 }; 1601 1602 // Combined sync handler - pulls remote changes first if needed, then pushes local ··· 1605 let on_refresh = props.on_refresh.clone(); 1606 let current_state = display_state; 1607 move |_: dioxus::events::MouseEvent| { 1608 if *sync_state.peek() == SyncState::Syncing { 1609 return; // Already syncing 1610 } ··· 1623 } 1624 }; 1625 1626 rsx! { 1627 div { 1628 class: "{class}", 1629 - title: if let Some(ref err) = *last_error.read() { err.clone() } else { label.to_string() }, 1630 onclick: on_sync_click, 1631 1632 span { class: "sync-icon", "{icon}" } 1633 span { class: "sync-label", "{label}" }
··· 239 } 240 } 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 + 278 /// Result of a sync operation. 279 #[derive(Clone, Debug)] 280 pub enum SyncResult { ··· 665 Ok(all_diffs) 666 } 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 + 678 /// Create the edit root record for an entry. 679 /// 680 /// Uploads the current Loro snapshot as a blob and creates an `sh.weaver.edit.root` 681 /// record referencing the entry (or draft key if unpublished). 682 /// 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. 685 pub async fn create_edit_root( 686 fetcher: &Fetcher, 687 doc: &EditorDocument, 688 draft_key: &str, 689 entry_uri: Option<&AtUri<'_>>, 690 entry_cid: Option<&Cid<'_>>, 691 + ) -> Result<CreateRootResult, WeaverError> { 692 let client = fetcher.get_client(); 693 let did = fetcher 694 .current_did() ··· 696 .ok_or_else(|| WeaverError::InvalidNotebook("Not authenticated".into()))?; 697 698 // For drafts, create the stub record first (makes it discoverable via listRecords) 699 + let draft_ref: Option<StrongRef<'static>> = if entry_uri.is_none() { 700 let rkey = extract_draft_rkey(draft_key); 701 + // Try to create draft stub, or get existing one 702 match create_draft_stub(fetcher, &did, &rkey).await { 703 + Ok((uri, cid)) => { 704 tracing::debug!("Created draft stub: {}", uri); 705 + Some(StrongRef::new().uri(uri).cid(cid).build()) 706 } 707 Err(e) => { 708 + // Check if it's a "record already exists" error 709 let err_str = e.to_string(); 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 { 741 tracing::warn!("Failed to create draft stub (continuing anyway): {}", e); 742 + None 743 } 744 } 745 } 746 + } else { 747 + None // Published entry, not a draft 748 + }; 749 750 // Export full snapshot 751 let snapshot = doc.export_snapshot(); ··· 789 .into_output() 790 .map_err(|e| WeaverError::InvalidNotebook(e.to_string()))?; 791 792 + Ok(CreateRootResult { 793 + root_uri: output.uri.into_static(), 794 + root_cid: output.cid.into_static(), 795 + draft_ref, 796 + }) 797 } 798 799 /// Create a diff record with updates since the last sync. ··· 912 if doc.edit_root().is_none() { 913 // First sync - create root 914 let create_start = crate::perf::now(); 915 + let result = create_edit_root( 916 fetcher, 917 doc, 918 draft_key, ··· 924 925 // Build StrongRef for the root 926 let root_ref = StrongRef::new() 927 + .uri(result.root_uri.clone()) 928 + .cid(result.root_cid.clone()) 929 .build(); 930 931 // Update document state ··· 933 doc.set_last_diff(None); 934 doc.mark_synced(); 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 + 944 let total_ms = crate::perf::now() - fn_start; 945 tracing::debug!(total_ms, create_ms, "sync_to_pds: created root"); 946 947 Ok(SyncResult::CreatedRoot { 948 + uri: result.root_uri, 949 + cid: result.root_cid, 950 }) 951 } else { 952 // Subsequent sync - create diff ··· 1009 /// Last seen diff URI per collaborator root (for incremental sync). 1010 /// Maps root URI -> last diff URI we've imported from that root. 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>, 1014 } 1015 1016 /// Fetch a blob from the PDS. ··· 1101 let merged_doc = LoroDoc::new(); 1102 let mut our_root_ref: Option<StrongRef<'static>> = None; 1103 let mut our_last_diff_ref: Option<StrongRef<'static>> = None; 1104 + let mut merged_doc_ref: Option<DocRef<'static>> = None; 1105 let mut updated_last_seen = last_seen_diffs.clone(); 1106 1107 // Get current user's DID to identify "our" root for sync state tracking ··· 1150 updated_last_seen.insert(uri.clone(), last_diff.uri.clone().into_static()); 1151 } 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 + 1158 // Track "our" root/diff refs for sync state (used when syncing back) 1159 // We want to track our own edit.root so subsequent diffs go to the right place 1160 let is_our_root = current_did.as_ref().is_some_and(|did| root_did == *did); ··· 1190 root_snapshot: merged_snapshot.into(), 1191 diff_updates: vec![], // Already merged into snapshot 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"), 1194 })) 1195 } 1196 ··· 1232 .uri(root_uri.clone()) 1233 .cid(root_cid.into_static()) 1234 .build(); 1235 + 1236 + // Extract the DocRef from the root record 1237 + let doc_ref = root_output.value.doc.into_static(); 1238 1239 // Fetch the root snapshot blob 1240 let root_snapshot = fetch_blob( ··· 1254 root_snapshot, 1255 diff_updates: vec![], 1256 last_seen_diffs: std::collections::HashMap::new(), 1257 + doc_ref, 1258 })); 1259 } 1260 ··· 1276 } 1277 } 1278 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(); 1287 1288 let diff_response = fetcher 1289 .client ··· 1337 root_snapshot, 1338 diff_updates, 1339 last_seen_diffs: std::collections::HashMap::new(), 1340 + doc_ref, 1341 })) 1342 } 1343 ··· 1403 synced_version: None, // Local-only, never synced to PDS 1404 last_seen_diffs: std::collections::HashMap::new(), 1405 resolved_content, 1406 + notebook_uri: local.notebook_uri, // Restored from localStorage 1407 })) 1408 } 1409 ··· 1427 // Capture the version after loading all PDS state - this is our sync baseline 1428 let synced_version = Some(doc.oplog_vv()); 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 + 1436 let resolved_content = 1437 prefetch_embeds_from_doc(&doc, fetcher, owner_ident.as_deref()).await; 1438 1439 Ok(Some(LoadedDocState { 1440 doc, 1441 + entry_ref, 1442 edit_root: Some(pds.root_ref), 1443 last_diff: pds.last_diff_ref, 1444 synced_version, // Just loaded from PDS, fully synced 1445 last_seen_diffs: pds.last_seen_diffs, 1446 resolved_content, 1447 + notebook_uri: None, // PDS-only, notebook context comes from target_notebook 1448 })) 1449 } 1450 ··· 1497 synced_version: Some(pds_version), 1498 last_seen_diffs: pds.last_seen_diffs, 1499 resolved_content, 1500 + notebook_uri: local.notebook_uri, // Restored from localStorage 1501 })) 1502 } 1503 } ··· 1534 pub document: EditorDocument, 1535 /// Draft key for this document 1536 pub draft_key: String, 1537 + /// Auto-sync interval in milliseconds (0 to disable, default disabled) 1538 + #[props(default = 0)] 1539 pub auto_sync_interval_ms: u32, 1540 /// Callback to refresh/reload document from collaborators 1541 #[props(default)] ··· 1548 /// Sync status indicator with auto-sync functionality. 1549 /// 1550 /// Displays the current sync state and automatically syncs to PDS periodically. 1551 + /// Initially shows "Start syncing" until user activates sync, then auto-syncs. 1552 #[component] 1553 pub fn SyncStatus(props: SyncStatusProps) -> Element { 1554 let fetcher = use_context::<Fetcher>(); 1555 let auth_state = use_context::<Signal<AuthState>>(); 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 + 1567 // Sync state management 1568 let mut sync_state = use_signal(|| { 1569 if props.document.has_unsynced_changes() { ··· 1574 }); 1575 let mut last_error: Signal<Option<String>> = use_signal(|| None); 1576 1577 // Check if we're authenticated (drafts can sync via DraftRef even without entry) 1578 let is_authenticated = auth_state.read().is_authenticated(); 1579 1580 // Auto-sync trigger signal - set to true to trigger a sync 1581 let mut trigger_sync = use_signal(|| false); 1582 1583 + // Auto-sync timer - only triggers after sync has been activated 1584 { 1585 let doc_for_check = doc.clone(); 1586 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 + }); 1598 } 1599 1600 // Collaborator poll timer - checks for collaborator updates periodically ··· 1699 Ok(_) => { 1700 sync_state.set(SyncState::Synced); 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 + } 1707 tracing::debug!("Sync completed successfully"); 1708 } 1709 Err(e) => { ··· 1716 }); 1717 1718 // Determine display state (drafts can sync too via DraftRef) 1719 + let is_activated = *sync_activated.read(); 1720 let display_state = if !is_authenticated { 1721 SyncState::Disabled 1722 } else { 1723 *sync_state.read() 1724 }; 1725 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); 1779 }; 1780 1781 // Combined sync handler - pulls remote changes first if needed, then pushes local ··· 1784 let on_refresh = props.on_refresh.clone(); 1785 let current_state = display_state; 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 + 1792 if *sync_state.peek() == SyncState::Syncing { 1793 return; // Already syncing 1794 } ··· 1807 } 1808 }; 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 + 1821 rsx! { 1822 div { 1823 class: "{class}", 1824 + title: "{title}", 1825 onclick: on_sync_click, 1826 + onpointerdown: on_pointer_down, 1827 + onpointerup: on_pointer_up, 1828 + onpointerleave: on_pointer_leave, 1829 1830 span { class: "sync-icon", "{icon}" } 1831 span { class: "sync-label", "{label}" }
+8
crates/weaver-app/src/components/editor/worker.rs
··· 43 editing_uri: Option<SmolStr>, 44 /// Editing CID if editing existing entry 45 editing_cid: Option<SmolStr>, 46 }, 47 /// Start collab session (worker will spawn CollabNode) 48 StartCollab { ··· 100 editing_uri: Option<SmolStr>, 101 /// Editing CID 102 editing_cid: Option<SmolStr>, 103 /// Export timing in ms 104 export_ms: f64, 105 /// Encode timing in ms ··· 266 cursor_offset, 267 editing_uri, 268 editing_cid, 269 } => { 270 let Some(ref doc) = doc else { 271 if let Err(e) = scope ··· 314 cursor_offset, 315 editing_uri, 316 editing_cid, 317 export_ms, 318 encode_ms, 319 }) ··· 663 cursor_offset, 664 editing_uri, 665 editing_cid, 666 } => { 667 let Some(ref doc) = doc else { 668 if let Err(e) = scope ··· 707 cursor_offset, 708 editing_uri, 709 editing_cid, 710 export_ms, 711 encode_ms, 712 })
··· 43 editing_uri: Option<SmolStr>, 44 /// Editing CID if editing existing entry 45 editing_cid: Option<SmolStr>, 46 + /// Notebook URI for re-publishing 47 + notebook_uri: Option<SmolStr>, 48 }, 49 /// Start collab session (worker will spawn CollabNode) 50 StartCollab { ··· 102 editing_uri: Option<SmolStr>, 103 /// Editing CID 104 editing_cid: Option<SmolStr>, 105 + /// Notebook URI for re-publishing 106 + notebook_uri: Option<SmolStr>, 107 /// Export timing in ms 108 export_ms: f64, 109 /// Encode timing in ms ··· 270 cursor_offset, 271 editing_uri, 272 editing_cid, 273 + notebook_uri, 274 } => { 275 let Some(ref doc) = doc else { 276 if let Err(e) = scope ··· 319 cursor_offset, 320 editing_uri, 321 editing_cid, 322 + notebook_uri, 323 export_ms, 324 encode_ms, 325 }) ··· 669 cursor_offset, 670 editing_uri, 671 editing_cid, 672 + notebook_uri, 673 } => { 674 let Some(ref doc) = doc else { 675 if let Err(e) = scope ··· 714 cursor_offset, 715 editing_uri, 716 editing_cid, 717 + notebook_uri, 718 export_ms, 719 encode_ms, 720 })
+1 -2
crates/weaver-app/src/components/entry.rs
··· 605 606 div { class: "entry-meta-info", 607 // Authors 608 - if !entry_view.authors.is_empty() { 609 div { class: "entry-authors", 610 AuthorList { 611 authors: entry_view.authors.clone(), 612 owner_ident: Some(ident.clone()), 613 } 614 } 615 - } 616 617 // Date 618 div { class: "entry-date",
··· 605 606 div { class: "entry-meta-info", 607 // Authors 608 div { class: "entry-authors", 609 AuthorList { 610 authors: entry_view.authors.clone(), 611 owner_ident: Some(ident.clone()), 612 } 613 } 614 + 615 616 // Date 617 div { class: "entry-date",
+17 -2
crates/weaver-app/src/views/drafts.rs
··· 5 use crate::components::button::{Button, ButtonVariant}; 6 use crate::components::dialog::{DialogContent, DialogDescription, DialogRoot, DialogTitle}; 7 use crate::components::editor::{list_drafts_from_pds, RemoteDraft}; 8 - use crate::components::editor::{delete_draft, list_drafts}; 9 use crate::fetch::Fetcher; 10 use dioxus::prelude::*; 11 use jacquard::smol_str::{SmolStr, format_smolstr}; ··· 39 let mut local_drafts = use_signal(list_drafts); 40 let mut show_delete_confirm = use_signal(|| None::<String>); 41 42 // Fetch remote drafts from PDS (depends on auth state to re-run when logged in) 43 let remote_drafts_resource = use_resource(move || { 44 - let fetcher = fetcher.clone(); 45 let _did = auth_state.read().did.clone(); // Track auth state for reactivity 46 async move { list_drafts_from_pds(&fetcher).await.ok().unwrap_or_default() } 47 }); ··· 131 }); 132 133 let mut handle_delete = move |key: String| { 134 delete_draft(&key); 135 local_drafts.set(list_drafts()); 136 show_delete_confirm.set(None); 137 }; 138 139 rsx! {
··· 5 use crate::components::button::{Button, ButtonVariant}; 6 use crate::components::dialog::{DialogContent, DialogDescription, DialogRoot, DialogTitle}; 7 use crate::components::editor::{list_drafts_from_pds, RemoteDraft}; 8 + use crate::components::editor::{delete_draft, delete_draft_from_pds, list_drafts}; 9 use crate::fetch::Fetcher; 10 use dioxus::prelude::*; 11 use jacquard::smol_str::{SmolStr, format_smolstr}; ··· 39 let mut local_drafts = use_signal(list_drafts); 40 let mut show_delete_confirm = use_signal(|| None::<String>); 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 + 46 // Fetch remote drafts from PDS (depends on auth state to re-run when logged in) 47 let remote_drafts_resource = use_resource(move || { 48 + let fetcher = fetcher_for_resource.clone(); 49 let _did = auth_state.read().did.clone(); // Track auth state for reactivity 50 async move { list_drafts_from_pds(&fetcher).await.ok().unwrap_or_default() } 51 }); ··· 135 }); 136 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 142 delete_draft(&key); 143 local_drafts.set(list_drafts()); 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 + }); 152 }; 153 154 rsx! {
+6 -1
crates/weaver-app/src/views/home.rs
··· 10 /// Pinned content items - can be notebooks or entries 11 #[derive(Clone, PartialEq)] 12 pub enum PinnedItem { 13 Notebook { 14 ident: AtIdentifier<'static>, 15 title: SmolStr, ··· 31 // }, 32 PinnedItem::Entry { 33 ident: AtIdentifier::Did(Did::new_static("did:plc:yfvwmnlztr4dwkb7hwz55r2g").unwrap()), 34 - rkey: SmolStr::new_static("3m4rbphjzt62b"), 35 }, 36 ] 37 } 38
··· 10 /// Pinned content items - can be notebooks or entries 11 #[derive(Clone, PartialEq)] 12 pub enum PinnedItem { 13 + #[allow(dead_code)] 14 Notebook { 15 ident: AtIdentifier<'static>, 16 title: SmolStr, ··· 32 // }, 33 PinnedItem::Entry { 34 ident: AtIdentifier::Did(Did::new_static("did:plc:yfvwmnlztr4dwkb7hwz55r2g").unwrap()), 35 + rkey: SmolStr::new_static("3m7ysqf2z5s22"), 36 }, 37 + // PinnedItem::Entry { 38 + // ident: AtIdentifier::Did(Did::new_static("did:plc:yfvwmnlztr4dwkb7hwz55r2g").unwrap()), 39 + // rkey: SmolStr::new_static("3m4rbphjzt62b"), 40 + // }, 41 ] 42 } 43
+1 -1
crates/weaver-cli/src/main.rs
··· 388 // Use WeaverExt to upsert entry (handles notebook + entry creation/updates) 389 use jacquard::http_client::HttpClient; 390 use weaver_common::WeaverExt; 391 - let (entry_ref, was_created) = agent 392 .upsert_entry(&title, entry_title.as_ref(), entry, None) 393 .await?; 394
··· 388 // Use WeaverExt to upsert entry (handles notebook + entry creation/updates) 389 use jacquard::http_client::HttpClient; 390 use weaver_common::WeaverExt; 391 + let (entry_ref, _, was_created) = agent 392 .upsert_entry(&title, entry_title.as_ref(), entry, None) 393 .await?; 394
+97 -27
crates/weaver-common/src/agent.rs
··· 123 } 124 } 125 126 /// Find or create a notebook by title, returning its URI and entry list 127 /// 128 /// If the notebook doesn't exist, creates it with the given DID as author. ··· 208 } 209 } 210 211 - /// Find or create an entry within a notebook 212 /// 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. 221 /// 222 - /// Returns (entry_ref, was_created) 223 - fn upsert_entry( 224 &self, 225 - notebook_title: &str, 226 entry_title: &str, 227 entry: entry::Entry<'_>, 228 existing_rkey: Option<&str>, 229 - ) -> impl Future<Output = Result<(StrongRef<'static>, bool), WeaverError>> 230 where 231 Self: Sized, 232 { 233 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 // If we have an existing rkey, try to find and update that specific entry 243 if let Some(rkey) = existing_rkey { 244 // Check if this entry exists in the notebook by comparing rkeys ··· 259 .uri(output.uri.into_static()) 260 .cid(output.cid.into_static()) 261 .build(); 262 - return Ok((updated_ref, false)); 263 } 264 } 265 ··· 283 }) 284 .await?; 285 286 - return Ok((new_ref, true)); 287 } 288 289 - // No existing rkey - use title-based matching (original behavior) 290 291 // Fast path: if notebook is empty, skip search and create directly 292 if entry_refs.is_empty() { ··· 307 }) 308 .await?; 309 310 - return Ok((new_ref, true)); 311 } 312 313 // Check if entry with this title exists in the notebook ··· 331 .uri(output.uri.into_static()) 332 .cid(output.cid.into_static()) 333 .build(); 334 - return Ok((updated_ref, false)); 335 } 336 } 337 } ··· 355 }) 356 .await?; 357 358 - Ok((new_ref, true)) 359 } 360 } 361
··· 123 } 124 } 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 + 166 /// Find or create a notebook by title, returning its URI and entry list 167 /// 168 /// If the notebook doesn't exist, creates it with the given DID as author. ··· 248 } 249 } 250 251 + /// Find or create an entry within a notebook (with pre-fetched notebook data) 252 /// 253 + /// This variant accepts notebook URI and entry_refs directly to avoid redundant 254 + /// notebook lookups when the caller has already fetched this data. 255 /// 256 + /// Returns (entry_ref, notebook_uri, was_created) 257 + fn upsert_entry_with_notebook( 258 &self, 259 + notebook_uri: AtUri<'static>, 260 + entry_refs: Vec<StrongRef<'static>>, 261 entry_title: &str, 262 entry: entry::Entry<'_>, 263 existing_rkey: Option<&str>, 264 + ) -> impl Future<Output = Result<(StrongRef<'static>, AtUri<'static>, bool), WeaverError>> 265 where 266 Self: Sized, 267 { 268 async move { 269 // If we have an existing rkey, try to find and update that specific entry 270 if let Some(rkey) = existing_rkey { 271 // Check if this entry exists in the notebook by comparing rkeys ··· 286 .uri(output.uri.into_static()) 287 .cid(output.cid.into_static()) 288 .build(); 289 + return Ok((updated_ref, notebook_uri, false)); 290 } 291 } 292 ··· 310 }) 311 .await?; 312 313 + return Ok((new_ref, notebook_uri, true)); 314 } 315 316 + // No existing rkey - use title-based matching 317 318 // Fast path: if notebook is empty, skip search and create directly 319 if entry_refs.is_empty() { ··· 334 }) 335 .await?; 336 337 + return Ok((new_ref, notebook_uri, true)); 338 } 339 340 // Check if entry with this title exists in the notebook ··· 358 .uri(output.uri.into_static()) 359 .cid(output.cid.into_static()) 360 .build(); 361 + return Ok((updated_ref, notebook_uri, false)); 362 } 363 } 364 } ··· 382 }) 383 .await?; 384 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 429 } 430 } 431
+4
crates/weaver-index/Cargo.toml
··· 64 chrono = { version = "0.4", features = ["serde"] } 65 smol_str = "0.3" 66 67 # CID handling (for CAR block lookups) 68 cid = "0.11" 69 ··· 73 dashmap = "6" 74 include_dir = "0.7.4" 75 regex = "1" 76 77 # WebSocket (for tap consumer) 78 tokio-tungstenite = { version = "0.26", features = ["native-tls"] }
··· 64 chrono = { version = "0.4", features = ["serde"] } 65 smol_str = "0.3" 66 67 + # CRDT (for parsing draft snapshots) 68 + loro = { workspace = true } 69 + 70 # CID handling (for CAR block lookups) 71 cid = "0.11" 72 ··· 76 dashmap = "6" 77 include_dir = "0.7.4" 78 regex = "1" 79 + mini-moka = { git = "https://github.com/moka-rs/mini-moka", rev = "da864e849f5d034f32e02197fee9bb5d5af36d3d" } 80 81 # WebSocket (for tap consumer) 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 3 use clap::{Parser, Subcommand}; 4 use tracing::{error, info, warn}; 5 use weaver_index::clickhouse::InserterConfig; 6 use weaver_index::clickhouse::{Client, Migrator}; 7 use weaver_index::config::{ ··· 9 }; 10 use weaver_index::firehose::FirehoseConsumer; 11 use weaver_index::server::{AppState, ServerConfig, TelemetryConfig, telemetry}; 12 - use weaver_index::{FirehoseIndexer, ServiceIdentity, TapIndexer, load_cursor}; 13 14 #[derive(Parser)] 15 #[command(name = "indexer")] ··· 165 }); 166 let did_doc = identity.did_document_with_service(&server_config.service_did, &service_endpoint); 167 168 - // Create separate clients for indexer and server 169 let indexer_client = Client::new(&ch_config)?; 170 let server_client = Client::new(&ch_config)?; 171 172 // Build AppState for server 173 let state = AppState::new( ··· 208 tokio::spawn(async move { indexer.run().await }) 209 } 210 }; 211 212 // Run server, monitoring indexer health 213 tokio::select! {
··· 2 3 use clap::{Parser, Subcommand}; 4 use tracing::{error, info, warn}; 5 + use jacquard::client::UnauthenticatedSession; 6 use weaver_index::clickhouse::InserterConfig; 7 use weaver_index::clickhouse::{Client, Migrator}; 8 use weaver_index::config::{ ··· 10 }; 11 use weaver_index::firehose::FirehoseConsumer; 12 use weaver_index::server::{AppState, ServerConfig, TelemetryConfig, telemetry}; 13 + use weaver_index::{ 14 + DraftTitleTaskConfig, FirehoseIndexer, ServiceIdentity, TapIndexer, load_cursor, 15 + run_draft_title_task, 16 + }; 17 18 #[derive(Parser)] 19 #[command(name = "indexer")] ··· 169 }); 170 let did_doc = identity.did_document_with_service(&server_config.service_did, &service_endpoint); 171 172 + // Create separate clients for indexer, server, and background tasks 173 let indexer_client = Client::new(&ch_config)?; 174 let server_client = Client::new(&ch_config)?; 175 + let task_client = std::sync::Arc::new(Client::new(&ch_config)?); 176 177 // Build AppState for server 178 let state = AppState::new( ··· 213 tokio::spawn(async move { indexer.run().await }) 214 } 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 + )); 224 225 // Run server, monitoring indexer health 226 tokio::select! {
+2 -2
crates/weaver-index/src/clickhouse.rs
··· 7 pub use client::{Client, TableSize}; 8 pub use migrations::{DbObject, MigrationResult, Migrator, ObjectType}; 9 pub use queries::{ 10 - CollaboratorRow, EditHeadRow, EditNodeRow, EntryRow, HandleMappingRow, NotebookRow, 11 - ProfileCountsRow, ProfileRow, ProfileWithCounts, 12 }; 13 pub use resilient_inserter::{InserterConfig, ResilientRecordInserter}; 14 pub use schema::{
··· 7 pub use client::{Client, TableSize}; 8 pub use migrations::{DbObject, MigrationResult, Migrator, ObjectType}; 9 pub use queries::{ 10 + CollaboratorRow, EditChainNode, EditHeadRow, EditNodeRow, EntryRow, HandleMappingRow, 11 + NotebookRow, ProfileCountsRow, ProfileRow, ProfileWithCounts, StaleDraftRow, 12 }; 13 pub use resilient_inserter::{InserterConfig, ResilientRecordInserter}; 14 pub use schema::{
+1 -1
crates/weaver-index/src/clickhouse/queries.rs
··· 12 13 pub use collab::PermissionRow; 14 pub use collab_state::{CollaboratorRow, EditHeadRow}; 15 - pub use edit::EditNodeRow; 16 pub use identity::HandleMappingRow; 17 pub use notebooks::{EntryRow, NotebookRow}; 18 pub use profiles::{ProfileCountsRow, ProfileRow, ProfileWithCounts};
··· 12 13 pub use collab::PermissionRow; 14 pub use collab_state::{CollaboratorRow, EditHeadRow}; 15 + pub use edit::{EditChainNode, EditNodeRow, StaleDraftRow}; 16 pub use identity::HandleMappingRow; 17 pub use notebooks::{EntryRow, NotebookRow}; 18 pub use profiles::{ProfileCountsRow, ProfileRow, ProfileWithCounts};
+188 -3
crates/weaver-index/src/clickhouse/queries/edit.rs
··· 40 pub root_cid: SmolStr, 41 #[serde(with = "clickhouse::serde::chrono::datetime64::millis::option")] 42 pub last_edit_at: Option<chrono::DateTime<chrono::Utc>>, 43 } 44 45 impl Client { ··· 108 109 /// List drafts for an actor. 110 /// 111 - /// Returns draft records with associated edit root info if available. 112 pub async fn list_drafts( 113 &self, 114 actor_did: &str, 115 cursor: Option<i64>, 116 limit: i64, 117 ) -> Result<Vec<DraftWithRootRow>, IndexError> { 118 - // Query drafts table with LEFT JOIN to get associated edit roots 119 // Edit roots reference drafts via resource_type/did/rkey fields 120 let query = r#" 121 SELECT 122 d.did, ··· 126 COALESCE(e.did, '') AS root_did, 127 COALESCE(e.rkey, '') AS root_rkey, 128 COALESCE(e.cid, '') AS root_cid, 129 - e.created_at AS last_edit_at 130 FROM drafts d FINAL 131 LEFT JOIN ( 132 SELECT ··· 141 AND resource_type = 'draft' 142 AND deleted_at = toDateTime64(0, 3) 143 ) e ON e.resource_did = d.did AND e.resource_rkey = d.rkey 144 WHERE d.did = ? 145 AND d.deleted_at = toDateTime64(0, 3) 146 AND (? = 0 OR toUnixTimestamp64Milli(d.created_at) < ?) ··· 165 })?; 166 167 Ok(rows) 168 } 169 }
··· 40 pub root_cid: SmolStr, 41 #[serde(with = "clickhouse::serde::chrono::datetime64::millis::option")] 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, 77 } 78 79 impl Client { ··· 142 143 /// List drafts for an actor. 144 /// 145 + /// Returns draft records with associated edit root info and title if available. 146 pub async fn list_drafts( 147 &self, 148 actor_did: &str, 149 cursor: Option<i64>, 150 limit: i64, 151 ) -> Result<Vec<DraftWithRootRow>, IndexError> { 152 + // Query drafts table with LEFT JOINs to get edit roots and titles 153 // Edit roots reference drafts via resource_type/did/rkey fields 154 + // Titles are extracted from Loro snapshots by background task 155 let query = r#" 156 SELECT 157 d.did, ··· 161 COALESCE(e.did, '') AS root_did, 162 COALESCE(e.rkey, '') AS root_rkey, 163 COALESCE(e.cid, '') AS root_cid, 164 + e.created_at AS last_edit_at, 165 + COALESCE(t.title, '') AS title 166 FROM drafts d FINAL 167 LEFT JOIN ( 168 SELECT ··· 177 AND resource_type = 'draft' 178 AND deleted_at = toDateTime64(0, 3) 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 181 WHERE d.did = ? 182 AND d.deleted_at = toDateTime64(0, 3) 183 AND (? = 0 OR toUnixTimestamp64Milli(d.created_at) < ?) ··· 202 })?; 203 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(()) 353 } 354 }
+2 -2
crates/weaver-index/src/clickhouse/queries/notebooks.rs
··· 163 e.indexed_at AS indexed_at, 164 e.record AS record 165 FROM notebook_entries ne FINAL 166 - INNER JOIN entries FINAL AS e ON 167 e.did = ne.entry_did 168 AND e.rkey = ne.entry_rkey 169 AND e.deleted_at = toDateTime64(0, 3) ··· 731 e.indexed_at AS indexed_at, 732 e.record AS record 733 FROM notebook_entries ne FINAL 734 - INNER JOIN entries FINAL AS e ON 735 e.did = ne.entry_did 736 AND e.rkey = ne.entry_rkey 737 AND e.deleted_at = toDateTime64(0, 3)
··· 163 e.indexed_at AS indexed_at, 164 e.record AS record 165 FROM notebook_entries ne FINAL 166 + INNER JOIN entries e FINAL ON 167 e.did = ne.entry_did 168 AND e.rkey = ne.entry_rkey 169 AND e.deleted_at = toDateTime64(0, 3) ··· 731 e.indexed_at AS indexed_at, 732 e.record AS record 733 FROM notebook_entries ne FINAL 734 + INNER JOIN entries e FINAL ON 735 e.did = ne.entry_did 736 AND e.rkey = ne.entry_rkey 737 AND e.deleted_at = toDateTime64(0, 3)
+8
crates/weaver-index/src/endpoints/edit.rs
··· 369 370 let last_edit_at = row.last_edit_at.map(|dt| Datetime::new(dt.fixed_offset())); 371 372 drafts.push( 373 DraftView::new() 374 .uri(uri) ··· 376 .created_at(created_at) 377 .maybe_edit_root(edit_root) 378 .maybe_last_edit_at(last_edit_at) 379 .build(), 380 ); 381 }
··· 369 370 let last_edit_at = row.last_edit_at.map(|dt| Datetime::new(dt.fixed_offset())); 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 + 379 drafts.push( 380 DraftView::new() 381 .uri(uri) ··· 383 .created_at(created_at) 384 .maybe_edit_root(edit_root) 385 .maybe_last_edit_at(last_edit_at) 386 + .maybe_title(title) 387 .build(), 388 ); 389 }
+18 -1
crates/weaver-index/src/endpoints/notebook.rs
··· 152 XrpcErrorResponse::internal_error("Invalid CID stored") 153 })?; 154 155 // Hydrate entry authors 156 - let entry_authors = hydrate_authors(&entry_row.author_dids, &profile_map)?; 157 158 // Parse record JSON 159 let entry_record = parse_record_json(&entry_row.record)?;
··· 152 XrpcErrorResponse::internal_error("Invalid CID stored") 153 })?; 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 + 172 // Hydrate entry authors 173 + let entry_authors = hydrate_authors(&author_dids_vec, &profile_map)?; 174 175 // Parse record JSON 176 let entry_record = parse_record_json(&entry_row.record)?;
+4
crates/weaver-index/src/error.rs
··· 29 #[error(transparent)] 30 #[diagnostic(transparent)] 31 Sqlite(#[from] SqliteError), 32 } 33 34 /// HTTP server errors
··· 29 #[error(transparent)] 30 #[diagnostic(transparent)] 31 Sqlite(#[from] SqliteError), 32 + 33 + #[error("resource not found: {resource}")] 34 + #[diagnostic(code(index::not_found))] 35 + NotFound { resource: String }, 36 } 37 38 /// HTTP server errors
+2
crates/weaver-index/src/lib.rs
··· 9 pub mod service_identity; 10 pub mod sqlite; 11 pub mod tap; 12 13 pub use config::Config; 14 pub use error::{IndexError, Result}; ··· 17 pub use server::{AppState, ServerConfig}; 18 pub use service_identity::ServiceIdentity; 19 pub use sqlite::{ShardKey, ShardRouter, SqliteShard};
··· 9 pub mod service_identity; 10 pub mod sqlite; 11 pub mod tap; 12 + pub mod tasks; 13 14 pub use config::Config; 15 pub use error::{IndexError, Result}; ··· 18 pub use server::{AppState, ServerConfig}; 19 pub use service_identity::ServiceIdentity; 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 "state": { 42 "type": "markdown", 43 "state": { 44 - "file": "Writing the AppView Last.md", 45 "mode": "source", 46 "source": false 47 }, 48 "icon": "lucide-file", 49 - "title": "Writing the AppView Last" 50 } 51 } 52 ], ··· 199 "bases:Create new base": false 200 } 201 }, 202 - "active": "6029beecc3d03bce", 203 "lastOpenFiles": [ 204 "weaver_index_html.png", 205 "2025-12-14T23:32:19.png", 206 "diff_record.png", 207 "Why I rewrote pdsls in Rust (tm).md", 208 - "Writing the AppView Last.md", 209 "light_mode_excerpt.png", 210 "notebook_entry_preview.png", 211 "xkcd_345_excerpt.png", ··· 214 "Pasted image 20251114125028.png", 215 "invalid_record.png", 216 "Pasted image 20251114125031.png", 217 - "Pasted image 20251114121431.png", 218 "Arch.md", 219 "Weaver - Long-form writing.md" 220 ]
··· 41 "state": { 42 "type": "markdown", 43 "state": { 44 + "file": "Untitled.md", 45 "mode": "source", 46 "source": false 47 }, 48 "icon": "lucide-file", 49 + "title": "Untitled" 50 } 51 } 52 ], ··· 199 "bases:Create new base": false 200 } 201 }, 202 + "active": "2f6bdb6d2dce13ed", 203 "lastOpenFiles": [ 204 + "cargo_bloated.png", 205 + "Writing the AppView Last.md", 206 + "Untitled.md", 207 "weaver_index_html.png", 208 "2025-12-14T23:32:19.png", 209 "diff_record.png", 210 "Why I rewrote pdsls in Rust (tm).md", 211 "light_mode_excerpt.png", 212 "notebook_entry_preview.png", 213 "xkcd_345_excerpt.png", ··· 216 "Pasted image 20251114125028.png", 217 "invalid_record.png", 218 "Pasted image 20251114125031.png", 219 "Arch.md", 220 "Weaver - Long-form writing.md" 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.