this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Small review-sweep fixes: rotation log, metadata invalidation, retry API

Three independent findings from the review, small enough to land together.

tree_keeper's KeyringUpsert handler silently dropped rotation values
smaller than the last-seen one. That's correct behavior (we already
bootstrapped past them), but "silently" leaves no trace when something
actually is out of order. Add a debug log so stray backward replays
surface in traces.

useDirectoryMetadata has its own react-query cache key that tree
mutations never invalidated, so an upload/rename/move would leave the
parent-directory metadata view showing "[Encrypted]" placeholders until
its stale time elapsed. Invalidate the metadata prefix on every tree
mutation's onSettled — keepPreviousData keeps the active view from
flickering, inactive directories are inert refetches.

FileView.handleRetry called window.location.reload(), which nuked the
module-level preview + readme caches and replayed the whole cabinet
bootstrap for a single failed tree load. useDirectory now exposes
retry(): a generation counter bump re-runs loadTree and re-installs
the watcher in place, keeping the plaintext caches intact.

+40 -10
+5 -8
apps/web/src/components/cabinet/FileView.tsx
··· 115 115 isReady, 116 116 error, 117 117 resolvedDirectoryUri, 118 + retry, 118 119 } = useDirectory(keyringUri, targetDirectoryUri); 119 120 120 121 // Derive the resolved directory URI from the snapshot + pathSegments. ··· 506 507 507 508 const footerText = `${items.length} ${items.length === 1 ? "item" : "items"} · End-to-end encrypted`; 508 509 509 - // Retry after a load error — a full reload is the simplest way to 510 - // re-run the OpakeProvider's FileManagerCache construction and the 511 - // useDirectory loadTree. useDirectory doesn't expose an imperative 512 - // retry, and there's no dep change we can force while staying on the 513 - // same directoryUri, so going through the navigation layer is cheapest. 514 - const handleRetry = useCallback(() => { 515 - window.location.reload(); 516 - }, []); 510 + // Retry after a load error — bumps the useDirectory generation so the 511 + // effect re-runs loadTree and re-installs the watcher without a full 512 + // page reload (which would evict preview + readme caches too). 513 + const handleRetry = retry; 517 514 518 515 // ----------------------------------------------------------------- 519 516 // Side-panel preview
+14
crates/opake-core/src/indexer/tree_keeper/mod.rs
··· 313 313 // — don't affect name plaintext, so no-op. Cabinet keyrings 314 314 // don't apply here; those events carry a workspace keyring 315 315 // URI in `uri`. 316 + // 317 + // Rotation comparison is strictly monotonic: an equal value 318 + // is an SSE echo of the rotation we already applied, a lower 319 + // value is an out-of-order replay from after we bootstrapped 320 + // past it. Both are no-ops. A lower value with a *different* 321 + // content under the same rotation number would be a protocol 322 + // violation — log it so it surfaces in traces. 316 323 SseEvent::KeyringUpsert(record) => { 317 324 let Some(new_rotation) = record.rotation else { 318 325 return Ok(()); ··· 325 332 held.tree.invalidate_decrypted_names(); 326 333 let scope = TreeScope::Workspace(record.uri.clone()); 327 334 self.notify_scope(&scope); 335 + } else if new_rotation < held.rotation { 336 + log::debug!( 337 + "[tree_keeper] ignoring backward keyring rotation on {}: held={}, received={}", 338 + record.uri, 339 + held.rotation, 340 + new_rotation 341 + ); 328 342 } 329 343 } 330 344 // Keyring delete: the workspace tree becomes unreadable
+15 -2
packages/opake-react/src/hooks/use-directory.ts
··· 24 24 // arrives as the same tree state the mutation already wrote, and the 25 25 // WASM TreeKeeper dedupes at the record layer. 26 26 27 - import { useEffect, useState } from "react"; 27 + import { useCallback, useEffect, useState } from "react"; 28 28 import type { DirectoryTreeSnapshot, DirectoryWatcher, FileManager } from "@opake/sdk"; 29 29 import { useFileManager } from "./use-file-manager"; 30 30 ··· 41 41 * tree has no root yet (empty cabinet/workspace). 42 42 */ 43 43 readonly resolvedDirectoryUri: string | null; 44 + /** 45 + * Re-run loadTree and re-install the watcher. Intended for error 46 + * recovery — call from a retry button rather than as an ambient 47 + * refresh trigger. Idempotent: safely callable while the previous 48 + * attempt is still pending. 49 + */ 50 + readonly retry: () => void; 44 51 } 45 52 46 53 /** ··· 91 98 ): UseDirectoryResult { 92 99 const { fileManager, isReady: fmReady } = useFileManager(keyringUri); 93 100 const [commit, setCommit] = useState<Commit | null>(null); 101 + // Retry is a generation counter that invalidates the effect's deps 102 + // without touching the component's identity, so a fresh loadTree / 103 + // watcher install happens on demand. 104 + const [retryGeneration, setRetryGeneration] = useState(0); 105 + const retry = useCallback(() => setRetryGeneration((g) => g + 1), []); 94 106 95 107 useEffect(() => { 96 108 if (!fmReady || !fileManager) return; ··· 165 177 state.cancelled = true; 166 178 state.watcher?.close(); 167 179 }; 168 - }, [fileManager, fmReady, directoryUri]); 180 + }, [fileManager, fmReady, directoryUri, retryGeneration]); 169 181 170 182 // Only honor a commit whose keys match the current render's props. 171 183 const current = ··· 177 189 isReady: current?.snapshot != null, 178 190 error: current?.error ?? null, 179 191 resolvedDirectoryUri: current?.resolvedDirectoryUri ?? null, 192 + retry, 180 193 }; 181 194 }
+6
packages/opake-react/src/hooks/use-tree-mutation.ts
··· 77 77 78 78 onSettled: () => { 79 79 void queryClient.invalidateQueries({ queryKey: key }); 80 + // Metadata is keyed per-directory and useDirectoryMetadata has a 81 + // separate cache that doesn't observe tree mutations. Rather than 82 + // thread a directoryUri through every mutation signature, invalidate 83 + // all metadata prefixes — non-active directories are inert refetches 84 + // and keepPreviousData suppresses loading flicker on the active one. 85 + void queryClient.invalidateQueries({ queryKey: ["opake", "metadata"] }); 80 86 }, 81 87 }); 82 88 }