Skip to content

Cache workspace search facets (sessionStorage + optional server TTL) #530

Description

@zachdunn

Context

The in-web workspace files search typeahead (WorkspaceFileTable + GET /me/workspaces/:name/files/facets) already avoids re-walking metadata on every keystroke:

  • Facet keys load lazily on first menu focus, then stay in React state for the mount (facets is only set once).
  • Facet values load per key, once, into facetValues[key].
  • In-flight refs block duplicate concurrent requests.
  • Keystrokes only filter the already-loaded lists in buildSuggestions (capped at 50 keys / 50 values server-side).
  • Server queries use file_metadata_lookup_idx (workspace, meta_key, meta_value) and load values lazily so opening the menu is one grouped query, not N.

No product change is required for correctness or basic performance. This issue tracks optional caching if first-open latency or reload flicker becomes noticeable.

Problem

What is not cached today:

  1. No browser persistence — full page reload / remount refetches facets from the API.
  2. No server-side cache — every cold open runs the D1 GROUP BY for keys (and per-key value aggregates on first drill-in).
  3. Client fetch uses cache: "no-store"; the facets route has no HTTP/Cache-Control story.
  4. Session cache can go slightly stale after uploads (acceptable for typeahead; any longer-lived cache needs a short TTL, not a complex invalidation bus).

Proposed improvements (when needed)

1. Client: sessionStorage (or similar) for facet keys

  • Mirror the pattern in apps/web/src/lib/workspace-cache.ts (versioned payload + short TTL).
  • Persist facet keys (and optionally recently loaded values) per workspace for the tab session.
  • On focus: paint from cache immediately, optionally revalidate in background.
  • Win: kills empty/loading flicker after reload; zero server change.

2. Server: short-TTL cache of facetKeys(workspace)

  • e.g. KV (or existing cache pattern ~30–120s) keyed by workspace name.
  • Return the same { keys, truncated } shape from facetKeys in apps/api/src/file-metadata.ts / GET …/files/facets.
  • Soft-stale is fine; do not invent per-upload invalidation for autocomplete.
  • Win: reduces repeated D1 aggregation on cold opens / multi-tab use.

Explicit non-goals

  • Do not re-fetch facets on every keystroke (already avoided).
  • Do not precompute all key→values when the menu opens (lazy values stay).
  • Do not add a write-path invalidation bus unless D1 cost becomes material.
  • Do not materialize counts on write unless aggregates get large enough to profile as a problem.

When to prioritize

Signal Prefer
Menu “pops” empty after every reload Client sessionStorage
Slow first open on large workspaces / rising D1 cost on facets Server short-TTL cache
Stale suggestions after heavy upload bursts Soft refresh on idle/focus or after successful upload — not per keystroke

References

  • Client load/cache: apps/web/src/components/WorkspaceFileTable.tsx (loadFacets / loadFacetValues)
  • Suggestion filtering: apps/web/src/lib/workspace-search-suggest.ts
  • API route: apps/api/src/routes/me.tsGET /workspaces/:name/files/facets
  • D1 queries: apps/api/src/file-metadata.ts (facetKeys, facetValues, FACET_*_LIMIT)
  • Existing shell snapshot cache (pattern only): apps/web/src/lib/workspace-cache.ts

Acceptance (if/when implemented)

  • First focus after a reload can show keys without waiting on a cold D1 round-trip (client cache), or server cold open is cheaper under concurrent opens (server TTL).
  • Stale data is bounded by an explicit TTL; no long-lived wrong facet lists.
  • Existing caps (50 keys / 50 values), server-meta exclusion, and lazy value loading remain.
  • Tests cover cache hit/miss and TTL expiry for whatever layer ships.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions