diff --git a/.github/workflows/changelog-release.yml b/.github/workflows/changelog-release.yml index ff626e3..68846e8 100644 --- a/.github/workflows/changelog-release.yml +++ b/.github/workflows/changelog-release.yml @@ -57,13 +57,23 @@ jobs: run: | VERSION="${{ steps.changelog.outputs.version }}" sed -i "s/^version = \".*\"/version = \"$VERSION\"/" crates/gpui-query/Cargo.toml + # Keep the version literal in sync across the install snippets that + # external systems render statically (crates.io README, GitHub README, + # docs installation page). Mirrors the Cargo.toml bump above; both the + # bare `gpui-query = "x.y.z"` and table `version = "x.y.z"` forms. + sed -i -E \ + -e 's|gpui-query = "[^"]*"|gpui-query = "'"$VERSION"'"|g' \ + -e 's|gpui-query = [{] version = "[^"]*"|gpui-query = { version = "'"$VERSION"'"|g' \ + README.md \ + crates/gpui-query/README.md \ + web/src/content/docs/docs/getting-started/installation.mdx - name: Commit version bump if: steps.check_tag.outputs.exists == 'false' run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add crates/gpui-query/Cargo.toml + git add crates/gpui-query/Cargo.toml README.md crates/gpui-query/README.md web/src/content/docs/docs/getting-started/installation.mdx git diff --cached --quiet || git commit -m "chore: bump gpui-query version to v${{ steps.changelog.outputs.version }}" - name: Create Git tag @@ -115,8 +125,8 @@ jobs: env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - deploy-website: - needs: publish-main + deploy-web: + needs: [publish-main, check-and-release] runs-on: ubuntu-latest environment: prod defaults: @@ -124,19 +134,24 @@ jobs: working-directory: web steps: + # Check out the tagged commit (created by check-and-release) so the web + # build sees the version-bumped Cargo.toml — read at build time by the OG + # image and changelog page — and the synced markdown install snippets. - uses: actions/checkout@v4 + with: + ref: ${{ needs.check-and-release.outputs.tag }} - uses: oven-sh/setup-bun@v2 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 - name: Install web dependencies run: bun install --frozen-lockfile - - name: Install website dependencies - working-directory: website + - name: Install docs dependencies + working-directory: docs run: npm ci - name: Build diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 51ac77f..27384f0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,7 +5,6 @@ on: branches: [master] paths: - 'web/**' - - 'website/**' - 'shared/**' - '.github/workflows/deploy.yml' workflow_dispatch: @@ -29,13 +28,13 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 - name: Install web dependencies run: bun install --frozen-lockfile - - name: Install website dependencies - working-directory: website + - name: Install docs dependencies + working-directory: docs run: npm ci - name: Build diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 5c8d003..f36a7ee 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -4,7 +4,6 @@ on: pull_request: paths: - "web/**" - - "website/**" - "shared/**" - ".github/workflows/pr-checks.yml" @@ -26,15 +25,11 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 - name: Install web dependencies run: bun install --frozen-lockfile - - name: Install website dependencies - working-directory: website - run: npm ci - - name: Build dry-run run: bun run build env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 84da027..67c097f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,7 +39,7 @@ jobs: env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - deploy-website: + deploy-web: runs-on: ubuntu-latest needs: publish-main defaults: @@ -53,13 +53,13 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 - name: Install web dependencies run: bun install --frozen-lockfile - - name: Install website dependencies - working-directory: website + - name: Install docs dependencies + working-directory: docs run: npm ci - name: Build diff --git a/.gitignore b/.gitignore index 4b69cc6..9a95fc0 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ dist/ .output/ .tanstack/ -# Generated docs (built from website/ by build-docs.mjs) +# Generated docs (built from docs/ by build-docs.mjs) web/public/docs/ # OS @@ -20,3 +20,7 @@ Thumbs.db # Local Playwright/MCP artifacts .playwright-mcp/ + +.wrangler/cache + +.astro-migration-baseline/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 90ccb85..dc00d29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,52 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.0] - 2026-07-21 + +> Disk persistence, server-driven cache policy, and two new companion crates. + +### Added + +#### Persistence (`gpui-query` with the new `persist` feature) + +- `Persister` trait with async `load` / `save`, and the `PersistSnapshot` / `PersistedEntry` / `PersistError` / `PERSIST_VERSION` types that adapters serialize to and from. +- `QueryClient::persist_with(persister, opts, cx) -> PersistHandle`, a debounced driver that coalesces bursts of cache mutations into one snapshot per window (default 500 ms) and writes only entries younger than `max_age` (default 24 h). +- `hydrate(client, persister, filter, max_age, cx)` to restore a cold-start cache from disk, re-checking the persist version before applying entries. +- `PersistOptions` (`filter` / `max_age` / `debounce`) and `PersistFilter` (`Exact` / `Prefix` / `All`) to scope what gets persisted. +- `SerializerRegistry` / `DeserializerRegistry` for round-tripping typed values through `serde_json::Value` without leaking concrete types into the core layer. +- `NoopPersister` for tests and disabled-persistence modes. + +#### "Server wins" cache policy (`Fetched`) + +- `Fetched` fetcher result wrapper in `core`: return `Result, E>` to let a fetcher attach a server-derived `CachePolicy` that overrides the caller's per-query policy on success. +- `Fetched::new` (no override), `Fetched::with_policy` (override), and `Fetched::with_meta` (persist-gated opaque metadata, e.g. an HTTP `CacheMeta` for cheap `304` refetches after relaunch). +- `use_query_with_policy` and `fetch_query_with_policy` hooks that accept `Fetched`-returning fetchers. + +#### `gpui-query-http` — HTTP cache-header helpers (new companion crate) + +- `cache_policy_from_headers(&HeaderMap)` turns `Cache-Control` into a `CachePolicy` per [RFC 9111]: `no-store` / `no-cache` → `NoCache`, `max-age` / `s-maxage` → `Ttl`, and `stale-while-revalidate` → `StaleWhileRevalidate`. Directive names are matched case-insensitively and values may be quoted. +- `HttpCache` in-memory cache layer, generic over an `HttpBackend` trait so any request library can plug in. +- `BackendResponse`, `Conditionals` (ETag / `If-Modified-Since`), and a serializable `CacheMeta` for persistence round-trip. +- Optional `ReqwestBackend` behind the `reqwest` cargo feature; `reqwest` is never a hard dependency. +- Depends on `gpui-query` `core` only — no GPUI — keeping `http` / `bytes` / `reqwest` out of the core crate. + +#### `gpui-query-persist` — disk persistence adapter (new companion crate) + +- `FilePersister`, an atomic, durable `Persister`: each save writes to a sibling temp file, fsyncs it (issuing `F_FULLFSYNC` on macOS for true durability), renames it over the target, then fsyncs the parent directory on POSIX so a crash mid-write never corrupts the cache. +- Tolerant load: missing file → empty snapshot; corrupt JSON/bincode → logged warning + empty snapshot (no panic); version mismatch → typed `PersistError::VersionMismatch`. +- `PersistFormat::Json` (default, inspectable) and `PersistFormat::Bincode` (compact), plus `FilePersister::json` / `::bincode` / `::in_cache_dir` constructors. +- `PersistError` surfaces retryable Windows `ERROR_ACCESS_DENIED` (antivirus / concurrent reader) as a distinct `Permission` variant so callers can back off, while preserving the original `io::Error` chain elsewhere. + +### Changed + +- New `persist` cargo feature gates the persistence module (and the `serde_json` / `thiserror` deps); `core` and `client` stay free of it. +- The workspace gains two crates: `gpui-query-http` (core-only) and `gpui-query-persist` (pulls `persist` + `client` + `hook`). +- Error sanitization now strips connection strings, tokens, paths, emails, and hex keys from messages (documented in the main crate README). + ## [0.1.4] - 2026-06-17 +> Crate metadata and README improvements on crates.io. + ### Added - Author metadata (authors) and an Author section in both crate READMEs, linking the maintainer's website, GitHub, and X. @@ -14,6 +58,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.3] - 2026-06-14 +> Decoupled the legacy crate and fixed gpui version compatibility. + ### Changed - `gpui-query-legacy` is now fully decoupled from the main crate, with standalone docs, tests, and improved hook error handling, and it publishes independently. @@ -28,6 +74,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.2] - 2025-06-13 +> Single-workflow releases and an independent legacy crate. + ### Changed - The CI pipeline now publishes both crates in a single workflow run. The changelog-release workflow handles tag, GitHub Release, publish, and website deploy without needing a separate trigger. @@ -36,6 +84,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.1] - 2025-06-12 +> The v2 rewrite became the main crate; v1 lives on as gpui-query-legacy. + ### Changed - The v2 rewrite at `crates/gpui-query-v2` is now the main crate at `crates/gpui-query`. The old v1 code lives at `crates/gpui-query-legacy`. @@ -52,6 +102,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2025-06-10 +> Initial public release: core query system, client registry, and GPUI hooks. + ### Added #### Core Layer @@ -101,8 +153,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Initial public release -[0.1.4]: https://github.com/hmziqrs/gpui-query/releases/tag/v0.1.4 -[0.1.3]: https://github.com/hmziqrs/gpui-query/releases/tag/v0.1.3 -[0.1.2]: https://github.com/hmziqrs/gpui-query/releases/tag/v0.1.2 -[0.1.1]: https://github.com/hmziqrs/gpui-query/releases/tag/v0.1.1 -[0.1.0]: https://github.com/hmziqrs/gpui-query/releases/tag/v0.1.0 +[0.2.0]: https://github.com/freeoxide/gpui-query/releases/tag/v0.2.0 +[0.1.4]: https://github.com/freeoxide/gpui-query/releases/tag/v0.1.4 +[0.1.3]: https://github.com/freeoxide/gpui-query/releases/tag/v0.1.3 +[0.1.2]: https://github.com/freeoxide/gpui-query/releases/tag/v0.1.2 +[0.1.1]: https://github.com/freeoxide/gpui-query/releases/tag/v0.1.1 +[0.1.0]: https://github.com/freeoxide/gpui-query/releases/tag/v0.1.0 diff --git a/README.md b/README.md index c28d16d..7bcdaf8 100644 --- a/README.md +++ b/README.md @@ -18,21 +18,21 @@ The API mirrors what TanStack Query popularized in the JavaScript ecosystem: `us ```toml [dependencies] -gpui-query = "0.1.0" +gpui-query = "0.2.0" ``` This pulls in the `client` layer (which includes `core`). To use the declarative hooks: ```toml [dependencies] -gpui-query = { version = "0.1.0", features = ["hook"] } +gpui-query = { version = "0.2.0", features = ["hook"] } ``` To use only the core state machine with no GPUI dependency: ```toml [dependencies] -gpui-query = { version = "0.1.0", default-features = false, features = ["core"] } +gpui-query = { version = "0.2.0", default-features = false, features = ["core"] } ``` ## quick start @@ -85,20 +85,23 @@ fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { ## architecture -The library is three layers, each gated by a Cargo feature flag. +The library is four layers, each gated by a Cargo feature flag. **Core** (`feature = "core"`) is a serde-only state machine with zero framework coupling. This is where `QueryResource`, `MutationResource`, `CachePolicy`, `RetryPolicy`, `QueryKey`, and all the request lifecycle types live. You can use this layer in any Rust project, not just GPUI. -**Client** (`feature = "client"`, the default) builds on core and adds `QueryClient`, a GPUI `Global` that provides type-partitioned storage via `QueryBucket`. This layer handles garbage collection, cache invalidation, observers, persistence, and devtools diagnostics. +**Client** (`feature = "client"`, the default) builds on core and adds `QueryClient`, a GPUI `Global` that provides type-partitioned storage via `QueryBucket`. This layer handles garbage collection, cache invalidation, observers, and devtools diagnostics. **Hook** (`feature = "hook"`) provides the declarative hooks (`use_query`, `use_mutation`, `use_infinite_query`) that wire the client layer into GPUI views. All hooks return `(Entity, Subscription)` tuples. +**Persistence** (`feature = "persist"`) adds an async `Persister` trait, `QueryClient::persist_with` (debounced snapshot saves), the free `hydrate()` function for cold-start restore, and typed (de)serializer registries. See the [Persistence guide](https://gpui-query.freeoxide.com/docs/guides/persistence). + ```toml [features] default = ["client"] core = [] client = ["core", "dep:gpui"] hook = ["client"] +persist = ["client", "hook", "dep:serde_json", "dep:thiserror"] ``` ## queries @@ -216,33 +219,39 @@ Retry delay is `base * 2^attempt`, capped at `max_delay`. The fetcher's `QuerySi ## persistence -Implement `QueryPersister` to serialize and restore query state across app restarts: +Enable the `persist` feature to save and restore the cache across restarts. Implement the async `Persister` trait, then drive it with `QueryClient::persist_with` (debounced snapshot saves) and the free `hydrate` function (cold-start restore): ```rust -use gpui_query::{QueryPersister, DehydratedEntry}; - -struct FilePersister; +use std::time::Duration; +use gpui_query::client::{ + QueryClient, Persister, PersistSnapshot, PersistError, PersistOptions, PersistFilter, +}; -impl QueryPersister for FilePersister { - fn load(&self) -> Vec { - // Read from disk, database, etc. - } +struct MyPersister; // your backend: file, db, kv, … - fn save(&self, entries: Vec) { - // Write to disk, database, etc. - } +impl Persister for MyPersister { + async fn load(&self) -> Result { /* … */ } + async fn save(&self, _snapshot: &PersistSnapshot) -> Result<(), PersistError> { /* … */ } } + +// Debounced saves: coalesces bursts of cache mutations into one snapshot. +let _handle = client.persist_with(MyPersister, PersistOptions::default(), cx); + +// Cold start: re-prime the cache from the persister (needs &mut QueryClient). +gpui_query::client::hydrate( + &mut client, &MyPersister, &PersistFilter::All, Duration::from_secs(86_400), cx, +).await.ok(); ``` -Call `client.dehydrate()` to extract state and `client.hydrate(entries)` to restore it. +Only `Success` entries with a registered serializer are persisted; the typed round-trip is driven by `QueryClient::register_serializer` / `register_deserializer`. The companion crate **`gpui-query-persist`** ships a ready-made atomic disk adapter (`FilePersister`). See the [Persistence guide](https://gpui-query.freeoxide.com/docs/guides/persistence). ## other things worth knowing -`QueryObserver` and `MutationObserver` wrap entities and only call `cx.notify()` when the status actually changes, cutting down on unnecessary re-renders. +`QueryObserver` and `MutationObserver` wrap entities and only call `cx.notify()` when the status changes. This avoids unnecessary re-renders. `QueryError::sanitized()` redacts connection strings, bearer tokens, file paths, emails, and hex keys from error messages. Useful for logging without leaking secrets. -`use_query_select` projects a `QueryResource` through a `SelectTransform`, giving you a `MappedQueryResource` that derives values from cached data without extra fetches. +`use_query_select` projects a `QueryResource` through a `SelectTransform` to produce a `MappedQueryResource` that derives values from cached data. No extra fetches are needed. `ClientDiagnostic`, `QueryDiagnostic`, and `MutationDiagnostic` give you runtime introspection of the query client's internal state for debugging. @@ -252,9 +261,9 @@ Garbage collection runs on idle resources older than `gc_time_ms` (default: 5 mi ## links -- Documentation: -- Website: -- Source: +- Documentation: +- Website: +- Source: - GPUI framework: - TanStack Query: diff --git a/astro-migration.mdx b/astro-migration.mdx new file mode 100644 index 0000000..c794e37 --- /dev/null +++ b/astro-migration.mdx @@ -0,0 +1,782 @@ +--- +sidebar_position: 6 +title: Web and docs migration to Astro + Starlight +description: "Audited implementation plan for migrating gpui-query from TanStack Start and Docusaurus to Astro with Starlight documentation while preserving URLs, SEO, themes, chart tokens, components, search, and Cloudflare Pages deployment." +--- + +This page is the working plan for migrating the `web/` app from TanStack Start to Astro and the documentation from Docusaurus to Starlight. The goal is not a redesign. The goal is to preserve the current public behavior, visual system, docs URLs, SEO output, generated assets, search, and Cloudflare Pages deployment while replacing the framework surface. + +## Audit summary + +The previous Astro plan was missing the required Starlight migration. It treated `docs/` as a Docusaurus site that would continue to be built separately and copied into `web/public/docs`. That is no longer the target. + +Required corrections: + +- Use Starlight for documentation, not Docusaurus. +- Prefer one final Astro build in `web/` that owns both marketing/blog pages and Starlight docs. +- Keep docs under `/docs/**`. +- Keep the current visual system: shared tokens, fonts, dark mode behavior, chart colors, components, icon policy, and generated OG image style. +- Replace Docusaurus-specific docs assumptions: `docusaurus.config.ts`, `_category_.json`, `sidebars.ts`, Infima CSS variables, swizzled logo, Docusaurus client analytics, and Docusaurus build/copy script. +- Convert docs content carefully: sidebar order, root intro page, internal links, admonitions/asides, code highlighting, static assets, SEO fallback image, and generated markdown/llms scripts. +- Keep one Pagefind index for the final combined output. Do not let Starlight and the existing post-build Pagefind step create competing `/pagefind` indexes. + +Repo audit notes: + +- Current docs content is mostly portable Markdown/MDX. +- Current docs ordering depends on Docusaurus `sidebar_position` and `_category_.json`. +- The `advanced` section has duplicate `sidebar_position: 2` values for `devtools.mdx` and `observers.mdx`; use an explicit Starlight sidebar order instead of relying only on autogeneration. +- Current docs root depends on `docs/docs/intro.mdx` with `slug: /`; in Starlight this should become `web/src/content/docs/docs/index.mdx`. +- Current docs contain root-absolute links like `/api/queries`, `/guides/caching`, and `/getting-started/installation`. Under Starlight at `/docs`, these must become `/docs/api/queries`, `/docs/guides/caching`, `/docs/getting-started/installation`, or relative links. +- Current docs styling in `docs/src/css/custom.css` is Docusaurus/Infima-specific. Reuse the design intent and tokens, not the selectors as-is. +- Current chart colors only exist as CSS variables in `web/src/styles.css`; no chart component or chart library was found. Preserve those variables and move them into shared token scope before adding Starlight docs components that may use charts. +- Current `web/README.md`, `web/AGENTS.md`, `justfile`, `web/scripts/build-docs.ts`, `web/scripts/generate-og-images.ts`, `web/scripts/generate-llms-txt.ts`, and `web/scripts/generate-md-alt.ts` all contain Docusaurus or TanStack Start assumptions that must be updated. + +## Current state + +The repository has three surfaces that the migration must respect: + +- `web/`: marketing/blog site currently built with TanStack Start, Vite+, React, Tailwind v4, Pagefind, Firebase Analytics, and Cloudflare Pages. +- `docs/`: Docusaurus documentation site, currently built separately and copied into `web/public/docs`. +- Rust crates: not part of this migration, except that website and docs links point to crate APIs, changelog entries, and release metadata. + +Important existing behavior: + +- The deployed output is static content from `web/dist/client`. +- Canonical URLs intentionally avoid trailing slashes for app pages. +- The current build emits flat files such as `faq.html`, `blog.html`, and `privacy.html` so Cloudflare Pages does not redirect canonical app URLs. +- Documentation remains publicly served under `/docs/`. +- Pagefind indexes the final combined output, including docs and blog pages. +- Blog posts live in `web/src/content/blog/*.mdx`. +- Docs pages currently live in `docs/docs/**/*.mdx`. +- OG images, `llms.txt`, `llms-full.txt`, and Markdown alternatives are generated by scripts in `web/scripts`. +- There is no current server-function or API-route surface in `web/src`. + +The first Astro target should therefore be a static Astro site, not an SSR Worker app. + +## Target architecture + +Use Astro static output for the full public site and Starlight for documentation. + +Final target: + +- `web/astro.config.mjs` owns the Astro build. +- Marketing/blog/legal pages live in `web/src/pages`. +- Blog posts stay in `web/src/content/blog`. +- Starlight docs live in `web/src/content/docs/docs`. +- `web/src/content/docs/docs/index.mdx` renders `/docs/`. +- `web/src/content/docs/docs/getting-started/installation.mdx` renders `/docs/getting-started/installation`. +- `web/dist/client` remains the deploy directory. +- Cloudflare Pages remains the deploy target. + +Do not keep Docusaurus as a permanent second build. A temporary side-by-side phase is fine for parity comparison, but the final migration should remove the Docusaurus app and the copy-to-`web/public/docs` pipeline. + +Do not introduce `@astrojs/cloudflare` unless we add on-demand routes, API handlers, or Cloudflare bindings later. + +## Astro and Starlight configuration + +Use a single static Astro config with React islands, MDX, Tailwind, and Starlight: + +```js +// web/astro.config.mjs +import { defineConfig } from "astro/config"; +import react from "@astrojs/react"; +import mdx from "@astrojs/mdx"; +import starlight from "@astrojs/starlight"; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + site: "https://gpui-query.freeoxide.com", + trailingSlash: "never", + output: "static", + outDir: "dist/client", + build: { + format: "file", + }, + integrations: [ + react(), + mdx(), + starlight({ + title: "gpui-query Docs", + logo: { + src: "./src/assets/logo.svg", + }, + customCss: ["./src/styles/starlight.css"], + // First cut: keep the existing post-build Pagefind index for the + // combined marketing/blog/docs output. Re-enable Starlight search only + // if it is wired to the same final index or replaces the global search. + pagefind: false, + social: [ + { + icon: "github", + label: "GitHub", + href: "https://github.com/freeoxide/gpui-query", + }, + ], + sidebar: [ + { label: "Introduction", link: "/docs/" }, + { + label: "Getting Started", + items: [ + { label: "Installation", link: "/docs/getting-started/installation" }, + { label: "Quick Start", link: "/docs/getting-started/quick-start" }, + ], + }, + { + label: "API Reference", + items: [ + { label: "Queries", link: "/docs/api/queries" }, + { label: "Mutations", link: "/docs/api/mutations" }, + { label: "Infinite Queries", link: "/docs/api/infinite-queries" }, + { label: "QueryClient", link: "/docs/api/query-client" }, + ], + }, + { + label: "Guides", + items: [ + { label: "Caching", link: "/docs/guides/caching" }, + { label: "Error handling", link: "/docs/guides/error-handling" }, + { label: "Retry", link: "/docs/guides/retry" }, + { label: "Persistence", link: "/docs/guides/persistence" }, + { label: "Query Keys", link: "/docs/guides/query-keys" }, + { label: "The Select Pattern", link: "/docs/guides/select-pattern" }, + ], + }, + { + label: "Advanced", + items: [ + { label: "Devtools", link: "/docs/advanced/devtools" }, + { label: "Observers", link: "/docs/advanced/observers" }, + { label: "gpui-query vs. raw async", link: "/docs/advanced/comparison" }, + { label: "API Reference", link: "/docs/advanced/api-reference" }, + { label: "Migrating from v1 to v2", link: "/docs/advanced/migration" }, + ], + }, + ], + credits: false, + disable404Route: true, + }), + ], + vite: { + plugins: [tailwindcss()], + resolve: { + alias: { + "#": new URL("./src", import.meta.url).pathname, + "@": new URL("./src", import.meta.url).pathname, + }, + }, + }, +}); +``` + +The sidebar is intentionally explicit for the first cut because the current Docusaurus docs have one duplicate `sidebar_position` and many root-absolute links. During implementation, verify that Starlight route IDs and generated links match `/docs/**` exactly before considering autogeneration. + +Do not set Astro's `base` to `/docs`. The whole site is served from the domain root. The docs subpath comes from placing Starlight content under `src/content/docs/docs/**`. + +Add a combined content config: + +```ts +// web/src/content.config.ts +import { defineCollection } from "astro:content"; +import { glob } from "astro/loaders"; +import { z } from "astro/zod"; +import { docsLoader } from "@astrojs/starlight/loaders"; +import { docsSchema } from "@astrojs/starlight/schema"; + +const blog = defineCollection({ + loader: glob({ pattern: "**/*.mdx", base: "./src/content/blog" }), + schema: z.object({ + title: z.string(), + description: z.string(), + date: z.coerce.date(), + author: z.string(), + tags: z.array(z.string()).optional(), + }), +}); + +export const collections = { + docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), + blog, +}; +``` + +Useful references: + +- Astro configuration: https://docs.astro.build/en/reference/configuration-reference/ +- Astro React integration: https://docs.astro.build/en/guides/integrations-guide/react/ +- Astro MDX integration: https://docs.astro.build/en/guides/integrations-guide/mdx/ +- Astro content collections: https://docs.astro.build/en/guides/content-collections/ +- Starlight manual setup: https://starlight.astro.build/manual-setup/ +- Starlight at a subpath: https://starlight.astro.build/manual-setup/#use-starlight-at-a-subpath +- Starlight sidebar: https://starlight.astro.build/guides/sidebar/ +- Starlight customization: https://starlight.astro.build/guides/customization/ +- Starlight configuration: https://starlight.astro.build/reference/configuration/ +- Starlight search: https://starlight.astro.build/guides/site-search/ + +## Route and content mapping + +| Current route or source | Astro/Starlight target | Migration notes | +| --- | --- | --- | +| `web/src/routes/__root.tsx` | `web/src/layouts/BaseLayout.astro` | Move HTML shell, theme bootstrap, global CSS, navbar, footer, analytics, and metadata defaults. | +| `web/src/routes/index.tsx` | `web/src/pages/index.astro` | Render the current V4 landing page. Keep interactive landing widgets as React islands initially. | +| `web/src/routes/blog/index.tsx` | `web/src/pages/blog.astro` | Replace custom frontmatter-only glob with Astro content collection queries. | +| `web/src/routes/blog/$slug.tsx` | `web/src/pages/blog/[slug].astro` | Use `getStaticPaths()` and `render(post)`. Preserve prev/next navigation and JSON-LD. | +| `web/src/routes/faq.tsx` | `web/src/pages/faq.astro` | Prefer static HTML or native `details`/`summary` before keeping a React accordion. Preserve FAQ JSON-LD. | +| `web/src/routes/changelog.tsx` | `web/src/pages/changelog.astro` | Static page. Keep release data accurate to `CHANGELOG.md`. | +| `web/src/routes/privacy.tsx` | `web/src/pages/privacy.astro` | Static legal page. | +| `web/src/routes/terms.tsx` | `web/src/pages/terms.astro` | Static legal page. | +| `web/src/routes/v1.tsx` through `v4.tsx` | omit or generate conditionally | Preview routes are currently disabled. Keep them out of sitemap and output unless the flag is enabled. | +| `docs/docs/intro.mdx` | `web/src/content/docs/docs/index.mdx` | Remove `slug: /`. Preserve title and description. This is `/docs/`. | +| `docs/docs/getting-started/*.mdx` | `web/src/content/docs/docs/getting-started/*.mdx` | Convert sidebar frontmatter and links. | +| `docs/docs/api/*.mdx` | `web/src/content/docs/docs/api/*.mdx` | Convert sidebar frontmatter and links. | +| `docs/docs/guides/*.mdx` | `web/src/content/docs/docs/guides/*.mdx` | Convert sidebar frontmatter and links. | +| `docs/docs/advanced/*.mdx` | `web/src/content/docs/docs/advanced/*.mdx` | Convert sidebar frontmatter and links. | +| `docs/docs/**/_category_.json` | Starlight `sidebar` config | Preserve labels, order, and expanded/collapsed intent. Remove after parity. | +| `docs/sidebars.ts` | Starlight `sidebar` config | Replace Docusaurus autogenerated sidebar. | +| `docs/static/img/logo.svg` | `web/src/assets/logo.svg` or `web/public/img/logo.svg` | Use Starlight logo config. Preserve header/homepage behavior. | +| `docs/src/theme/Logo/index.tsx` | Starlight component override if needed | Current Docusaurus swizzle makes the docs logo link to `/`. Preserve this with a Starlight override if default logo behavior links only to docs home. | +| `docs/src/css/custom.css` | `web/src/styles/starlight.css` | Rebuild against Starlight CSS variables and selectors. Keep visual intent, tokens, fonts, and density. | + +## Phase 1: Baseline and inventory + +1. Build the current site before changing frameworks. +2. Save a URL and artifact manifest from `web/dist/client`. +3. Save rendered HTML snapshots for representative pages: + - `/` + - `/blog` + - `/blog/cache-policies-explained` + - `/blog/cooperative-cancellation` + - `/faq` + - `/changelog` + - `/privacy` + - `/terms` + - `/docs/` + - `/docs/getting-started/installation` + - `/docs/api/queries` + - `/docs/guides/caching` + - `/docs/advanced/migration` +4. Record required generated files: + - `/index.html` + - `/blog.html` + - `/blog/*.html` + - `/faq.html` + - `/changelog.html` + - `/privacy.html` + - `/terms.html` + - `/docs/**` + - `/pagefind/**` + - `/llms.txt` + - `/llms-full.txt` + - `/docs/**/*.md` + - `/og/**` + - `/og-image.png` + - `/sitemap.xml` + - `/docs/sitemap.xml` +5. Record current theme and component details: + - `shared/tokens.css` + - `web/src/styles.css` + - `docs/src/css/custom.css` + - `web/components.json` + - `web/src/components/ui/**` + - `web/src/components/navbar.tsx` + - `web/src/components/search-dialog.tsx` + - `web/src/components/theme-toggle.tsx` +6. Record docs content details: + - `_category_.json` labels and order + - `sidebar_position` + - `slug` + - root-absolute links + - `:::` admonitions + - static assets + - code block language coverage + +## Phase 2: Scaffold Astro and Starlight + +Add Astro dependencies while keeping TanStack and Docusaurus dependencies in place for comparison: + +```bash +cd web +bun add astro @astrojs/react @astrojs/mdx @astrojs/starlight +``` + +Add temporary scripts so old and new builds can be compared: + +```json +{ + "scripts": { + "astro:dev": "astro dev --port 3000", + "astro:build": "astro build", + "astro:preview": "astro preview" + } +} +``` + +Create: + +- `web/astro.config.mjs` +- `web/src/content.config.ts` +- `web/src/layouts/BaseLayout.astro` +- `web/src/components/HeadMeta.astro` or a small metadata helper +- `web/src/styles/starlight.css` +- `web/src/pages/404.astro` +- `web/src/content/docs/docs/**` + +Do not remove the old routes until the Astro output matches. + +## Phase 3: Starlight docs migration + +Move documentation content into Starlight: + +1. Copy `docs/docs/**/*.mdx` to `web/src/content/docs/docs/**/*.mdx`. +2. Rename `docs/docs/intro.mdx` to `web/src/content/docs/docs/index.mdx`. +3. Remove Docusaurus-only `slug: /` from the new docs index. +4. Convert `sidebar_position` to Starlight sidebar ordering. +5. Convert `_category_.json` labels and positions to the Starlight `sidebar` config. +6. Rewrite root-absolute internal docs links: + - `/getting-started/installation` -> `/docs/getting-started/installation` + - `/api/queries` -> `/docs/api/queries` + - `/guides/caching` -> `/docs/guides/caching` + - `/advanced/migration` -> `/docs/advanced/migration` +7. Verify all current `:::note`, `:::tip`, and `:::caution` blocks render as Starlight asides. Convert only the blocks Starlight does not accept. +8. Preserve Rust, TOML, shell, TypeScript, and TSX code highlighting. Starlight uses Astro/Shiki rather than Docusaurus Prism, so compare light and dark code blocks visually. +9. Move docs static assets into Astro/Starlight paths: + - logo + - favicon + - docs OG fallback image +10. Preserve docs metadata: + - title + - description + - canonical URL + - docs OG fallback + - docs sitemap +11. Replace the Docusaurus analytics client module with a Starlight/Astro-compatible analytics script or component. +12. Keep `docs/` until parity is proven, then remove Docusaurus files and dependencies. + +Do not copy the old Docusaurus build into `web/public/docs` in the final architecture. + +### Docs inventory to preserve + +Use this as the exact initial Starlight sidebar/content inventory. Autogeneration is acceptable only after these routes, labels, and order are verified. + +| Current source | Target route | Title | Order notes | +| --- | --- | --- | --- | +| `docs/docs/intro.mdx` | `/docs/` | Introduction | Top-level docs root. Remove `slug: /`. | +| `docs/docs/getting-started/installation.mdx` | `/docs/getting-started/installation` | Installation | Getting Started, position 1. | +| `docs/docs/getting-started/quick-start.mdx` | `/docs/getting-started/quick-start` | Quick Start | Getting Started, position 2. | +| `docs/docs/api/queries.mdx` | `/docs/api/queries` | Queries | API Reference, position 1. | +| `docs/docs/api/mutations.mdx` | `/docs/api/mutations` | Mutations | API Reference, position 2. | +| `docs/docs/api/infinite-queries.mdx` | `/docs/api/infinite-queries` | Infinite Queries | API Reference, position 3. | +| `docs/docs/api/query-client.mdx` | `/docs/api/query-client` | QueryClient | API Reference, position 4. | +| `docs/docs/guides/caching.mdx` | `/docs/guides/caching` | Caching | Guides, position 1. | +| `docs/docs/guides/error-handling.mdx` | `/docs/guides/error-handling` | Error handling | Guides, position 2. | +| `docs/docs/guides/retry.mdx` | `/docs/guides/retry` | Retry | Guides, position 3. | +| `docs/docs/guides/persistence.mdx` | `/docs/guides/persistence` | Persistence | Guides, position 4. | +| `docs/docs/guides/query-keys.mdx` | `/docs/guides/query-keys` | Query Keys | Guides, position 5. | +| `docs/docs/guides/select-pattern.mdx` | `/docs/guides/select-pattern` | The Select Pattern | Guides, position 6. | +| `docs/docs/advanced/devtools.mdx` | `/docs/advanced/devtools` | Devtools | Advanced, explicit before Observers. | +| `docs/docs/advanced/observers.mdx` | `/docs/advanced/observers` | Observers | Advanced, duplicate old position 2. Keep explicit. | +| `docs/docs/advanced/comparison.mdx` | `/docs/advanced/comparison` | gpui-query vs. raw async | Advanced, position 3. | +| `docs/docs/advanced/api-reference.mdx` | `/docs/advanced/api-reference` | API Reference | Advanced, position 4. | +| `docs/docs/advanced/migration.mdx` | `/docs/advanced/migration` | Migrating from v1 to v2 | Advanced, position 5. | + +Top-level section order from `_category_.json`: + +1. Introduction +2. Getting Started +3. API Reference +4. Guides +5. Advanced + +Root-absolute docs links are widespread across the current docs, not isolated examples. Rewrite all links that currently start with `/api/`, `/guides/`, `/getting-started/`, or `/advanced/` before comparing the Starlight output. + +## Phase 4: Theme, chart, and component parity + +The migration must keep the same themes, chart colors, and components. + +### Shared tokens + +Keep `shared/tokens.css` as the source of truth and make both Astro pages and Starlight docs consume it. + +Move or duplicate these current web-only variables into shared scope before docs components or charts rely on them: + +```css +:root { + --chart-1: oklch(0.828 0.111 230.318); + --chart-2: oklch(0.685 0.169 237.323); + --chart-3: oklch(0.588 0.158 241.966); + --chart-4: oklch(0.5 0.134 242.749); + --chart-5: oklch(0.443 0.11 240.79); +} +``` + +No current chart component or chart library was found in `web/` or `docs/`. Treat chart parity as token parity for this migration, then use those same variables for any future chart component. + +### Starlight CSS + +Do not paste `docs/src/css/custom.css` into Starlight unchanged. It targets Docusaurus and Infima selectors. Recreate the theme against Starlight variables and stable Starlight selectors. + +Minimum `web/src/styles/starlight.css` responsibilities: + +- import `shared/tokens.css` +- use the same fonts as the web app +- map Starlight accent, gray, background, border, text, code, sidebar, and TOC colors to shared tokens +- expose `--chart-1` through `--chart-5` +- preserve the small-radius/sharp visual identity +- preserve light and dark mode +- preserve code block density and table readability +- avoid broad global resets that break Starlight internals + +### Theme behavior + +The existing site uses: + +- `.dark` on `` +- `[data-theme='dark']` in shared tokens for Docusaurus +- `localStorage.theme` +- theme-color meta updates + +Starlight has its own theme UI. The migration must define one site-wide theme contract: + +- Keep `localStorage.theme` as the existing web contract unless there is a deliberate reason to change it. +- Ensure Starlight dark mode sets selectors that shared tokens understand. +- Ensure marketing pages and docs use the same light/dark palette. +- Ensure docs theme changes update the page without fighting the marketing `ThemeToggle`. +- Preserve the dark-mode flash prevention script for Astro pages. +- Verify `meta[name="theme-color"]` is correct in light and dark mode. + +If Starlight's default theme selector uses a different local storage key, add a tiny bridge script or replace the Starlight theme control with a component override. + +### Components + +Preserve the existing component system first: + +- Keep `web/src/components/ui` during the first cut. +- Keep Base UI React components for interactive islands where replacing them adds risk. +- Keep the current navbar, theme toggle, mobile nav, and search behavior unless a Starlight override is required for docs parity. +- Keep Phosphor as the preferred icon family for new or touched shared UI because `web/components.json` declares `"iconLibrary": "phosphor"`. +- Do not do a migration-wide icon swap. +- Existing Lucide icons may stay where they already exist until a component is intentionally touched. +- If an Astro-only component needs an icon, first consider inline SVG copied from existing usage or a tiny local Astro icon wrapper. + +For docs MDX components: + +- Prefer Starlight built-ins for asides, badges, file trees, link cards, steps, and tabs when they match the current design. +- Add local Astro wrappers only when the current docs visual language needs a specific button, badge, card, callout, or chart treatment. +- Style any custom docs component from shared tokens, not from new one-off colors. +- Do not initialize a full shadcn/ui Astro template over this repo. +- Evaluate shadcn/ui for Astro only as a follow-up for components that are being rewritten anyway. + +## Phase 5: Astro shell and web pages + +Move these concerns out of `web/src/routes/__root.tsx`: + +- `html lang="en"` +- theme bootstrap script +- global stylesheet import +- favicon and manifest links +- skip-to-content link +- navbar +- footer +- Firebase Analytics loader +- development-only tools +- 404 and error UI + +The layout should accept explicit metadata props: + +```ts +type PageMeta = { + title: string; + description?: string; + canonicalPath: string; + image?: string; + robots?: string; + jsonLd?: unknown; +}; +``` + +Do not rely on implicit route metadata until canonical behavior has been verified. + +Migrate web pages from lowest to highest interaction risk: + +1. Legal pages: `privacy`, `terms`. +2. `changelog`. +3. `faq`. +4. `blog` index. +5. `blog/[slug]`. +6. Home page. +7. Disabled landing previews, if we decide to keep them. + +For each page: + +- Port route metadata first. +- Port visible HTML second. +- Compare rendered HTML for title, description, canonical, OG, Twitter, and JSON-LD tags. +- Verify generated output paths match the old no-trailing-slash app scheme. +- Verify links to `/docs/**` point to the new Starlight docs. + +## Phase 6: Blog content collections + +Keep blog posts in `web/src/content/blog/*.mdx`. + +Then: + +1. Rebuild `/blog` with `getCollection("blog")`. +2. Rebuild `/blog/[slug]` with `getStaticPaths()`. +3. Use `render(post)` for the MDX body. +4. Preserve the existing JSON-LD shape from `web/src/lib/seo.ts`. +5. Preserve generated OG paths: `/og/blog/.png`. +6. Preserve date sorting and prev/next post navigation. +7. Delete `web/src/lib/blog.ts`, `web/src/lib/blog-content.ts`, and `mdxFrontmatterOnly()` only after the blog output matches. + +## Phase 7: React island strategy + +The initial migration should keep React where the current component is interactive: + +- `Navbar` +- `ThemeToggle` +- `SearchDialog` +- mobile nav sheet +- landing demos with state, timers, mouse tracking, copy buttons, or tabs + +Use Astro components for static wrappers and pages. + +When a React component stays as an island: + +- Replace TanStack Router `Link` with plain `` or a small local link wrapper. +- Keep props serializable. +- Hydrate with the narrowest directive that works: + - `client:load` for navbar/search/theme controls. + - `client:visible` for below-the-fold landing demos. + - `client:idle` for analytics or non-critical enhancements. + +After parity, reduce React islands in a second pass: + +- Convert footer to Astro. +- Convert legal page wrappers to Astro. +- Convert FAQ to static `details`/`summary` if the design remains acceptable. +- Extract `buttonVariants` and `badgeVariants` into framework-neutral files so Astro and React components can share classes. + +## Phase 8: Search and Pagefind + +Current search behavior: + +- `web/src/components/search-dialog.tsx` lazy-loads `/pagefind/pagefind.js`. +- The build runs `npx pagefind --site dist/client` after all site output exists. +- The index includes marketing, blog, and docs pages. + +Starlight also has Pagefind-based search support. Final architecture must choose one Pagefind owner. + +Preferred first-cut policy: + +- Keep the existing post-build `npx pagefind --site dist/client` step because it indexes the combined site. +- Keep the existing custom `SearchDialog` for global search. +- Set `pagefind: false` in the Starlight config for the first cut so Starlight does not generate or expect a separate Pagefind index. +- Override Starlight's `Search` component only if docs need a search button in the Starlight header; the override should open or link to the same global search experience. +- If we keep Starlight's search UI, configure it to use the same final `/pagefind` index and verify there is only one `/pagefind` output. + +Validation: + +- Search opens from the marketing navbar. +- Search opens from docs if docs header exposes it. +- Results include `/docs/**` and `/blog/**`. +- Search result URLs use canonical no-trailing-slash app URLs and valid docs URLs. +- No duplicate Pagefind assets or stale indexes are emitted. + +## Phase 9: Build pipeline + +Keep the final output directory as `dist/client`. + +Target final build script: + +```json +{ + "scripts": { + "build": "rm -rf dist && node scripts/generate-og-images.ts && astro build && npx pagefind --site dist/client && node scripts/generate-llms-txt.ts --output dist/client && node scripts/generate-md-alt.ts --output dist/client" + } +} +``` + +Update scripts: + +- `scripts/build-docs.ts`: remove in the final architecture. It only exists for Docusaurus. +- `scripts/generate-og-images.ts`: stop writing docs OG fallback to `../docs/static/img/og-docs.png`; write it into `web/public/og/docs.png` or another Astro-owned public path. +- `scripts/generate-llms-txt.ts`: read docs from `web/src/content/docs/docs`, not `../docs/docs`. +- `scripts/generate-md-alt.ts`: read docs from `web/src/content/docs/docs`, not `../docs/docs`. +- `scripts/lib/docs-md.ts`: update comments and route resolution for Starlight-at-`/docs` content. Ensure docs index maps to `/docs/`, not `/docs/docs`. + +Do not generate Markdown alternatives before Astro build if Astro deletes `dist/client`; keep them after build. + +## Phase 10: Sitemap, robots, and SEO + +Preserve the current URL policy: + +- App pages have no trailing slash. +- Blog post `lastmod` comes from frontmatter dates. +- Disabled preview routes are excluded. +- Docs remain under `/docs/**`. +- `robots.txt` continues to advertise app and docs sitemaps unless we intentionally change the sitemap structure. + +Current expected sitemap entries: + +- `https://gpui-query.freeoxide.com/sitemap.xml` +- `https://gpui-query.freeoxide.com/docs/sitemap.xml` + +Starlight can generate sitemap data when `site` is configured, but verify whether it emits the exact `/docs/sitemap.xml` artifact we currently advertise. If it cannot preserve the existing docs sitemap path, create a custom endpoint or update `robots.txt` only after an explicit SEO decision. + +SEO parity checks: + +- App canonical tags keep no trailing slash. +- Docs canonical tags are under `/docs/**`. +- Blog JSON-LD still renders as `BlogPosting`. +- Blog index JSON-LD still renders as `Blog`. +- FAQ JSON-LD still includes every visible question and answer. +- Changelog JSON-LD still reports the latest version. +- Homepage JSON-LD still renders as `SoftwareSourceCode`. +- OG images resolve for homepage, blog, blog posts, changelog, FAQ, and docs. +- `robots.txt` does not point at a missing sitemap. + +## Phase 11: Cloudflare deployment and cleanup + +After the Astro build is the default: + +1. Keep Cloudflare Pages deploy from `dist/client`. +2. Remove TanStack Start's Worker entry from `web/wrangler.jsonc`. +3. Remove old `run_worker_first` rules if they only existed for TanStack Start dev routing. +4. Re-check `assets.html_handling`. The old config used `auto-trailing-slash`; verify it does not reintroduce redirects for canonical no-slash app URLs. +5. Update GitHub workflows only if deploy commands or output paths change. +6. Update `justfile` from `Website (TanStack Start)` to `Website (Astro)`. +7. Update docs recipes from Docusaurus commands to Astro/Starlight commands. +8. Update `web/README.md`. +9. Update `web/AGENTS.md` so it no longer instructs contributors to use Vite+ or TanStack Start. +10. Update footer text from "Built with TanStack Start" to "Built with Astro". + +## Phase 12: Dependency cleanup + +Remove these only after imports and scripts are gone: + +- `@tanstack/react-start` +- `@tanstack/react-router` +- `@tanstack/router-plugin` +- `@tanstack/devtools-vite` +- `@cloudflare/vite-plugin`, if static Pages no longer needs it +- `vite-plus` +- Vite+ overrides for `vite` and `vitest`, if no longer needed +- old `web/vite.config.ts` +- TanStack route tree files +- Docusaurus packages in `docs/package.json` +- `docs/package-lock.json` +- `docs/docusaurus.config.ts` +- `docs/sidebars.ts` +- `docs/src/theme/Logo/index.tsx` +- `docs/src/firebaseAnalytics.ts` +- Docusaurus-specific CSS after it has been ported to Starlight + +Keep initially: + +- `react` +- `react-dom` +- `@astrojs/react` +- `@astrojs/mdx` +- `@astrojs/starlight` +- `@base-ui/react` +- `@phosphor-icons/react` +- existing `lucide-react` usage until those components are touched +- `class-variance-authority` +- `clsx` +- `tailwind-merge` +- `firebase` +- `pagefind` +- `tailwindcss` +- `@tailwindcss/vite` +- `@tailwindcss/typography`, if blog prose still uses it +- `shiki` or Astro/Starlight's code highlighting dependency path + +## Validation checklist + +Run: + +```bash +cd web +bun install --frozen-lockfile +bun run build +npx wrangler pages dev dist/client +``` + +Then verify these routes: + +- `/` +- `/blog` +- `/blog/cache-policies-explained` +- `/blog/cooperative-cancellation` +- `/faq` +- `/changelog` +- `/privacy` +- `/terms` +- `/docs/` +- `/docs/getting-started/installation` +- `/docs/getting-started/quick-start` +- `/docs/api/queries` +- `/docs/api/mutations` +- `/docs/api/infinite-queries` +- `/docs/api/query-client` +- `/docs/guides/caching` +- `/docs/guides/error-handling` +- `/docs/guides/retry` +- `/docs/guides/persistence` +- `/docs/guides/query-keys` +- `/docs/guides/select-pattern` +- `/docs/advanced/comparison` +- `/docs/advanced/devtools` +- `/docs/advanced/observers` +- `/docs/advanced/api-reference` +- `/docs/advanced/migration` +- `/pagefind/pagefind.js` +- `/llms.txt` +- `/llms-full.txt` +- `/sitemap.xml` +- `/docs/sitemap.xml` + +Artifact checks: + +- No generated app page unexpectedly moves from `foo.html` to `foo/index.html`. +- `/docs/` resolves without a 404. +- Docs internal links do not point to root `/api/**`, `/guides/**`, `/advanced/**`, or `/getting-started/**`. +- Generated Markdown alternatives exist under `/docs/**/*.md`. +- `llms.txt` URLs point to `/docs/**`. +- OG docs image is generated from the same OG template as the rest of the site. +- There is only one final Pagefind index. + +SEO checks: + +- Canonical tags match the expected URL list. +- App pages do not redirect from canonical URLs. +- Blog JSON-LD still renders as `BlogPosting`. +- FAQ JSON-LD still includes every visible question and answer. +- Changelog JSON-LD still reports the latest version. +- Docs pages have title and description metadata. +- OG images resolve. +- `robots.txt` advertises only existing sitemaps. + +UI checks: + +- Marketing pages match the current theme. +- Starlight docs match the current docs theme closely enough to count as a migration, not a redesign. +- Light mode and dark mode work on app pages. +- Light mode and dark mode work on docs pages. +- Theme selection stays in sync when moving between app and docs. +- Chart color variables are available in app and docs CSS. +- Navbar mobile menu works. +- Search opens with the button and keyboard shortcut. +- Pagefind returns docs and blog results after a production build. +- Landing interactive panels hydrate only where needed. +- No major layout shift appears from delayed islands. +- Code blocks, tables, asides, badges, and cards are readable in light and dark mode. + +## Merge strategy + +Use three PRs if possible: + +1. Astro web migration with React islands preserved and Docusaurus still available for comparison. +2. Starlight docs migration with URL, theme, search, sitemap, and generated artifact parity. +3. Cleanup PR that removes TanStack Start, Docusaurus, Vite+ assumptions, and temporary comparison code. + +The first production-facing Astro PR should be boring: same pages, same URLs, same design, smaller framework surface. The Starlight PR should be judged by docs parity first: same `/docs/**` URLs, same theme, same content, same search visibility, and no missing generated docs artifacts. diff --git a/combined.svg b/combined.svg new file mode 100644 index 0000000..50b49d7 --- /dev/null +++ b/combined.svg @@ -0,0 +1,458 @@ + + + + + + + + + + + diff --git a/crates/gpui-query-http/Cargo.toml b/crates/gpui-query-http/Cargo.toml index b058a73..65707b7 100644 --- a/crates/gpui-query-http/Cargo.toml +++ b/crates/gpui-query-http/Cargo.toml @@ -2,12 +2,13 @@ name = "gpui-query-http" version = "0.1.0" edition = "2024" +readme = "README.md" description = "HTTP cache-header helpers for gpui-query (server-wins CachePolicy + HttpCache)" -repository = "https://github.com/hmziqrs/gpui-query" +repository = "https://github.com/freeoxide/gpui-query" license = "MIT" keywords = ["gpui", "http", "caching", "cache-control", "query"] categories = ["asynchronous", "caching", "web-programming::http-client"] -homepage = "https://gpui-query.hmziq.xyz" +homepage = "https://gpui-query.freeoxide.com" authors = ["hmziqrs"] [features] diff --git a/crates/gpui-query-http/README.md b/crates/gpui-query-http/README.md new file mode 100644 index 0000000..6ab9776 --- /dev/null +++ b/crates/gpui-query-http/README.md @@ -0,0 +1,88 @@ +# gpui-query-http + +HTTP cache-header helpers for [gpui-query](https://crates.io/crates/gpui-query). Turn server cache headers into a [`CachePolicy`](https://docs.rs/gpui-query/latest/gpui_query/core/enum.CachePolicy.html) ("server wins") and layer an in-memory `HttpCache` over any HTTP client. + +gpui-query's core cache policies (`NoCache`, `Ttl`, `StaleWhileRevalidate`) are caller-supplied. When the resource you are fetching is an HTTP response, the server already knows how long it should live: `Cache-Control: max-age=600, stale-while-revalidate=120`. This crate parses those headers into the right `CachePolicy` and adds a URL-keyed cache that revalidates with conditional `GET`s. + +It depends on gpui-query's **`core`** feature only (no GPUI), so it is usable from any async context. `reqwest` is one *optional* backend, never a hard dependency. + +## Install + +```toml +[dependencies] +gpui-query-http = "0.1.0" +``` + +The default feature set is library-agnostic: parse headers and bring your own `HttpBackend`. To use the shipped reqwest backend: + +```toml +[dependencies] +gpui-query-http = { version = "0.1.0", features = ["reqwest"] } +``` + +The usage examples also reference `gpui-query` (for `core::{CachePolicy, Fetched}`) and the `http` crate (for `HeaderMap`). Add them alongside if you haven't: `cargo add gpui-query http`. + +## What it does + +- **Header → policy ("server wins").** `cache_policy_from_headers` reads RFC 9111 `Cache-Control` directives and returns the matching `gpui_query::core::CachePolicy`. +- **In-memory HTTP cache.** `HttpCache` wraps any `HttpBackend`: fresh entries short-circuit the network entirely, stale entries revalidate with `If-None-Match` / `If-Modified-Since`, and a `304 Not Modified` refreshes the entry without transferring a body. +- **Pluggable backend.** `HttpBackend` abstracts a single conditional `GET`. The crate ships `ReqwestBackend` behind the `reqwest` feature; any other client can implement the trait and feed `HttpCache::new`. +- **Serializable metadata.** `CacheMeta` (ETag, `Last-Modified`, `stored_at`, `fresh_for`, `stale_for`) is serde-serializable, so it round-trips through a persistence layer for cheap `304` refetches on cold start. +- **Typed errors.** `ParseError` (`InvalidMaxAge`, `InvalidStaleWhileRevalidate`) for malformed directives; `HttpError` for backend failures, bad policies, poisoned mutexes, and spurious `304`s. + +Parsing rules (priority order): + +1. `no-store` or `no-cache` → `CachePolicy::NoCache` (short-circuits immediately). +2. `max-age=N` (seconds) → `CachePolicy::Ttl { ttl_ms: N * 1000 }`; add `stale-while-revalidate=M` → `CachePolicy::StaleWhileRevalidate { ttl_ms, stale_ms }`. +3. Otherwise → `CachePolicy::NoCache`. + +`s-maxage` takes precedence over `max-age` when both are set. Directive names are matched case-insensitively and values may be quoted (`max-age="600"`). + +## Usage + +Parse the response headers inside your fetcher and hand the server's policy to `Fetched::with_policy`. The resource adopts the server's TTL ("server wins"): + +```rust +use gpui_query_http::cache_policy_from_headers; +use gpui_query::core::{CachePolicy, Fetched}; +use http::HeaderMap; + +// `headers` comes from your HTTP response; `data` is the decoded body. +fn build_fetched(data: String, headers: &HeaderMap) -> Fetched { + let policy = cache_policy_from_headers(headers).unwrap_or(CachePolicy::NoCache); + Fetched::with_policy(data, policy) +} +``` + +Return that `Fetched` from a fetcher passed to [`gpui_query::use_query_with_policy`](https://docs.rs/gpui-query/latest/gpui_query/hook/fn.use_query_with_policy.html) (enable gpui-query's `hook` feature): the server policy replaces the caller-supplied policy for that resource immediately after a successful fetch. + +For the cache layer, wrap any `HttpBackend`. With the `reqwest` feature: + +```rust +use gpui_query_http::{HttpCache, ReqwestBackend}; + +let cache = HttpCache::new(ReqwestBackend::from_client(reqwest::Client::new())); + +// Fresh hit → no network call; stale → conditional GET; 304 → cached body. +let (body, policy, meta) = cache.fetch("https://example.test/data").await?; +``` + +## Links + +- Website: +- Docs: +- Source: +- gpui-query: +- RFC 9111 (HTTP Caching): + +## Author + +**hmziqrs** + +- Website: +- GitHub: +- X: + +## License + +MIT. See the [LICENSE](../../LICENSE) file for details. diff --git a/crates/gpui-query-http/src/cache.rs b/crates/gpui-query-http/src/cache.rs index 8c2f01b..b1b7e72 100644 --- a/crates/gpui-query-http/src/cache.rs +++ b/crates/gpui-query-http/src/cache.rs @@ -3,8 +3,8 @@ //! [`HttpCache`] wraps any [`crate::backend::HttpBackend`] and adds an in-memory //! cache keyed by URL string: fresh entries short-circuit the network entirely, //! stale entries are revalidated with conditional headers (`If-None-Match` / -//! `If-Modified-Since`) and a `304 Not Modified` response refreshes the -//! stored-at timestamp without transferring a body. +//! `If-Modified-Since`) and a `304 Not Modified` response re-serves the cached +//! body without transferring a new one. //! //! The cache is library-agnostic — it does not name `reqwest` anywhere. `reqwest` //! is just one optional backend ([`crate::reqwest_backend::ReqwestBackend`]). @@ -120,10 +120,7 @@ impl HttpCache { && meta.stored_at + meta.fresh_for > SystemTime::now() { let body = { - let guard = self - .bodies - .lock() - .map_err(|_| HttpError::Poisoned)?; + let guard = self.bodies.lock().map_err(|_| HttpError::Poisoned)?; guard.get(url).cloned() }; if let Some(body) = body { @@ -148,10 +145,7 @@ impl HttpCache { // (e) 304 — revalidation succeeded: serve the cached body. if resp.status == 304 { let body = { - let guard = self - .bodies - .lock() - .map_err(|_| HttpError::Poisoned)?; + let guard = self.bodies.lock().map_err(|_| HttpError::Poisoned)?; guard.get(url).cloned() }; let Some(body) = body else { @@ -207,17 +201,11 @@ impl HttpCache { // Store under both locks, dropping each guard before yielding. { - let mut bodies = self - .bodies - .lock() - .map_err(|_| HttpError::Poisoned)?; + let mut bodies = self.bodies.lock().map_err(|_| HttpError::Poisoned)?; bodies.insert(url.to_string(), body.clone()); } { - let mut meta_guard = self - .meta - .lock() - .map_err(|_| HttpError::Poisoned)?; + let mut meta_guard = self.meta.lock().map_err(|_| HttpError::Poisoned)?; meta_guard.insert(url.to_string(), meta.clone()); } diff --git a/crates/gpui-query-http/src/lib.rs b/crates/gpui-query-http/src/lib.rs index bc508b0..7b90213 100644 --- a/crates/gpui-query-http/src/lib.rs +++ b/crates/gpui-query-http/src/lib.rs @@ -49,6 +49,8 @@ pub mod reqwest_backend; pub use backend::{BackendResponse, Conditionals, HttpBackend}; pub use cache::{HttpCache, HttpError}; +#[cfg(feature = "reqwest")] +pub use reqwest_backend::ReqwestBackend; /// HTTP cache metadata extracted from a response. /// diff --git a/crates/gpui-query-legacy/Cargo.toml b/crates/gpui-query-legacy/Cargo.toml index 7dd2de1..4ba5ee3 100644 --- a/crates/gpui-query-legacy/Cargo.toml +++ b/crates/gpui-query-legacy/Cargo.toml @@ -3,11 +3,11 @@ name = "gpui-query-legacy" version = "0.1.4" edition = "2024" description = "TanStack Query-inspired async state management for GPUI (legacy, deprecated)" -repository = "https://github.com/hmziqrs/gpui-query" +repository = "https://github.com/freeoxide/gpui-query" license = "MIT" keywords = ["gpui", "async", "state-management", "query", "zed"] categories = ["asynchronous", "caching", "gui"] -homepage = "https://gpui-query.hmziq.xyz" +homepage = "https://gpui-query.freeoxide.com" authors = ["hmziqrs"] readme = "README.md" diff --git a/crates/gpui-query-legacy/README.md b/crates/gpui-query-legacy/README.md index 7412ced..fe91adf 100644 --- a/crates/gpui-query-legacy/README.md +++ b/crates/gpui-query-legacy/README.md @@ -12,7 +12,7 @@ Use **gpui-query** (v2) instead. The v2 crate provides: - **Fixed retry loops** and signal lifecycle - **Efficient re-renders** with status deduplication -See the migration guide at [https://gpui-query.hmziq.xyz/docs/](https://gpui-query.hmziq.xyz/docs/). +See the migration guide at [https://gpui-query.freeoxide.com/docs/](https://gpui-query.freeoxide.com/docs/). ## Author diff --git a/crates/gpui-query-legacy/src/lib.rs b/crates/gpui-query-legacy/src/lib.rs index 60e3edb..1ee972a 100644 --- a/crates/gpui-query-legacy/src/lib.rs +++ b/crates/gpui-query-legacy/src/lib.rs @@ -8,10 +8,10 @@ //! - Fixed retry loops and signal lifecycle //! - Efficient re-renders with status deduplication //! -//! See https://gpui-query.hmziq.xyz/docs/ for migration guidance. +//! See https://gpui-query.freeoxide.com/docs/ for migration guidance. #![deprecated( since = "0.1.0", - note = "gpui-query-legacy is deprecated. Use gpui-query (v2) instead. See https://gpui-query.hmziq.xyz/docs/ for migration guidance." + note = "gpui-query-legacy is deprecated. Use gpui-query (v2) instead. See https://gpui-query.freeoxide.com/docs/ for migration guidance." )] #[cfg(feature = "core")] diff --git a/crates/gpui-query-persist/Cargo.toml b/crates/gpui-query-persist/Cargo.toml index 37dd231..2481e2f 100644 --- a/crates/gpui-query-persist/Cargo.toml +++ b/crates/gpui-query-persist/Cargo.toml @@ -3,11 +3,12 @@ name = "gpui-query-persist" version = "0.1.0" edition = "2024" description = "Reference disk-based persistence adapter (FilePersister) for gpui-query" -repository = "https://github.com/hmziqrs/gpui-query" +repository = "https://github.com/freeoxide/gpui-query" +readme = "README.md" license = "MIT" keywords = ["gpui", "persistence", "cache", "query", "filesystem"] categories = ["asynchronous", "caching", "filesystem"] -homepage = "https://gpui-query.hmziq.xyz" +homepage = "https://gpui-query.freeoxide.com" authors = ["hmziqrs"] [features] diff --git a/crates/gpui-query-persist/README.md b/crates/gpui-query-persist/README.md new file mode 100644 index 0000000..9ac31b1 --- /dev/null +++ b/crates/gpui-query-persist/README.md @@ -0,0 +1,87 @@ +# gpui-query-persist + +Reference disk-backed [`Persister`](https://docs.rs/gpui-query/latest/gpui_query/client/trait.Persister.html) for [gpui-query](https://crates.io/crates/gpui-query), modeled after [TanStack Query Persisters](https://tanstack.com/query/latest/docs/framework/react/plugins/persistQueryClient). + +gpui-query's `persist` feature gives you the `Persister` trait and the `persist_with` driver, but leaves storage to you. This crate ships `FilePersister`, an atomic, durable, file-system adapter you can drop in: JSON or bincode, tolerant of missing or corrupt files, and safe to run on GPUI's background executor with no `tokio` required. + +## Install + +```sh +cargo add gpui-query-persist +``` + +```toml +[dependencies] +gpui-query-persist = "0.1.0" +``` + +The crate pulls in [gpui-query](https://crates.io/crates/gpui-query) with the `persist`, `client`, and `hook` features already enabled, plus `dirs`, `tempfile`, `bincode`, `serde`, `serde_json`, and `thiserror`. + +## What it does + +- **Atomic write.** Each save serializes the snapshot to a sibling `NamedTempFile` (via [`tempfile`]), fsyncs it (`F_FULLFSYNC` on macOS, plain `fsync` elsewhere), then renames it over the target. On POSIX the parent directory is fsynced after the replace so the rename survives power loss. +- **Tolerant load.** A missing file yields an empty snapshot. A corrupt or unparseable file is logged and treated as empty (no panic). A version mismatch returns `PersistError::VersionMismatch`, so callers can distinguish "corrupt" from "wrong format". +- **No `tokio`.** Writes are serialized through a `std::sync::Mutex` and run on GPUI's `background_executor`; reads take the same lock briefly. The persister performs synchronous `std::fs` I/O, which is what the background executor is designed for. + +## Quick start + +Hand a `FilePersister` to `QueryClient::persist_with` when your app starts. Keep the returned `PersistHandle` alive for as long as you want saves to continue. + +```rust +use gpui::App; +use gpui_query::client::{PersistOptions, QueryClient}; +use gpui_query_persist::FilePersister; + +App::new().run(|cx| { + cx.set_global(QueryClient::new()); + + // JSON at an explicit path: + let persister = FilePersister::json("cache/gpui-query.json"); + // ...or rooted at the OS cache dir: + // let persister = FilePersister::in_cache_dir("my-app").unwrap(); + + let _handle = cx.update_global::(|client, cx| { + client.persist_with(persister, PersistOptions::default(), cx) + }); + // ... your views +}); +``` + +### Constructors + +```rust +// Pick a format explicitly: +FilePersister::new("path/to/cache.bin", PersistFormat::Bincode); + +// JSON / bincode shortcuts: +FilePersister::json("path/to/cache.json"); +FilePersister::bincode("path/to/cache.bin"); + +// Rooted at the OS cache dir: //gpui-query-cache.json +FilePersister::in_cache_dir("my-app")?; // -> Result + +// Inspect the on-disk path: +FilePersister::json("path/to/cache.json").path(); // -> &Path +``` + +`PersistFormat` is `Json` (human-readable, the default for `in_cache_dir`) or `Bincode` (compact, not human-readable). `NoopPersister` is re-exported for tests or disabled modes. + +## Links + +- Website: +- Docs: +- Source: +- gpui-query: +- TanStack Query Persisters: + +## Author + +**hmziqrs** + +- Website: +- GitHub: +- X: + +## License + +MIT. See the [LICENSE](../../LICENSE) file for details. diff --git a/crates/gpui-query-persist/tests/file_persister.rs b/crates/gpui-query-persist/tests/file_persister.rs index c931c28..c2ec4c2 100644 --- a/crates/gpui-query-persist/tests/file_persister.rs +++ b/crates/gpui-query-persist/tests/file_persister.rs @@ -224,17 +224,20 @@ fn file_persister_large_snapshot_round_trips() { .unwrap_or_else(|| panic!("format {fmt:?}: sampled entry {sample_key} present")); assert_eq!(entry.cached_at, 1_700_000_000_000 + 9000); assert_eq!(entry.cache_policy, CachePolicy::Ttl { ttl_ms: 60_000 }); - assert_eq!(entry.value, serde_json::json!({ - "id": 9000, - "name": "User 9000", - "email": "user9000@example.test", - "active": true, - "tags": ["tag-9000", "member", "persisted"], - "profile": { - "bio": "Auto-generated biography for user 9000.", - "followers": 27000, - }, - })); + assert_eq!( + entry.value, + serde_json::json!({ + "id": 9000, + "name": "User 9000", + "email": "user9000@example.test", + "active": true, + "tags": ["tag-9000", "member", "persisted"], + "profile": { + "bio": "Auto-generated biography for user 9000.", + "followers": 27000, + }, + }) + ); } } @@ -246,10 +249,17 @@ fn file_persister_corrupt_bincode_yields_empty_snapshot() { // an empty snapshot without panicking or erroring. let dir = tempfile::tempdir().expect("tempdir"); let path = dir.path().join("cache.bin"); - std::fs::write(&path, b"\x00\x01\x02 this is definitely not bincode \xff\xfe garbage").expect("write garbage"); + std::fs::write( + &path, + b"\x00\x01\x02 this is definitely not bincode \xff\xfe garbage", + ) + .expect("write garbage"); let p = FilePersister::bincode(&path); let loaded = pollster::block_on(p.load()).expect("tolerant load"); - assert!(loaded.entries.is_empty(), "corrupt bincode → empty snapshot"); + assert!( + loaded.entries.is_empty(), + "corrupt bincode → empty snapshot" + ); assert_eq!(loaded.version, PERSIST_VERSION); } diff --git a/crates/gpui-query/Cargo.toml b/crates/gpui-query/Cargo.toml index 3ccd3b0..36e2d00 100644 --- a/crates/gpui-query/Cargo.toml +++ b/crates/gpui-query/Cargo.toml @@ -3,12 +3,12 @@ name = "gpui-query" version = "0.2.0" edition = "2024" description = "TanStack Query-inspired async state management for GPUI" -repository = "https://github.com/hmziqrs/gpui-query" +repository = "https://github.com/freeoxide/gpui-query" readme = "README.md" license = "MIT" keywords = ["gpui", "async", "state-management", "query", "zed"] categories = ["asynchronous", "caching", "gui"] -homepage = "https://gpui-query.hmziq.xyz" +homepage = "https://gpui-query.freeoxide.com" authors = ["hmziqrs"] [features] diff --git a/crates/gpui-query/README.md b/crates/gpui-query/README.md index dab45c2..94baec2 100644 --- a/crates/gpui-query/README.md +++ b/crates/gpui-query/README.md @@ -10,21 +10,21 @@ You write a fetcher. The library manages the lifecycle. ```toml [dependencies] -gpui-query = "0.1.4" +gpui-query = "0.2.0" ``` The default feature set includes the `client` layer. To use the declarative view hooks, enable the `hook` feature: ```toml [dependencies] -gpui-query = { version = "0.1.4", features = ["hook"] } +gpui-query = { version = "0.2.0", features = ["hook"] } ``` If you only want the core state machine without pulling in GPUI: ```toml [dependencies] -gpui-query = { version = "0.1.4", default-features = false, features = ["core"] } +gpui-query = { version = "0.2.0", default-features = false, features = ["core"] } ``` ## Quick start @@ -76,11 +76,12 @@ fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { ## Feature layers -The crate is split into three layers, each behind a feature flag: +The crate is split into four layers, each behind a feature flag: - `core` is a serde-only state machine with no framework coupling. `QueryResource`, `MutationResource`, `CachePolicy`, `RetryPolicy`, and `QueryKey` live here. You can use this layer in any Rust project. -- `client` is the default feature. It adds `QueryClient`, a GPUI `Global` that owns type-partitioned storage. It handles garbage collection, cache invalidation, observers, persistence, and devtools diagnostics. +- `client` is the default feature. It adds `QueryClient`, a GPUI `Global` that owns type-partitioned storage. It handles garbage collection, cache invalidation, observers, and devtools diagnostics. - `hook` provides the declarative hooks (`use_query`, `use_mutation`, `use_infinite_query`) that wire the client into GPUI views. All hooks return `(Entity, Subscription)` tuples. +- `persist` adds the async `Persister` trait, `QueryClient::persist_with` (debounced snapshot saves), the free `hydrate` function, and typed (de)serializer registries to restore a warm cache on cold start. A ready-made disk adapter ships in the companion [`gpui-query-persist`](https://crates.io/crates/gpui-query-persist) crate. ## What you get @@ -94,13 +95,13 @@ The crate is split into three layers, each behind a feature flag: - Mutation callbacks for success, error, and settled states. - Infinite queries for paginated data. - Error sanitization that strips connection strings, tokens, paths, emails, and hex keys from messages. -- Persistence through the `QueryPersister` trait. +- Async persistence through the `Persister` trait (`persist` feature); a disk adapter ships in the [`gpui-query-persist`](https://crates.io/crates/gpui-query-persist) crate, and HTTP cache-header support in [`gpui-query-http`](https://crates.io/crates/gpui-query-http). ## Links -- Website: -- Docs: -- Source: +- Website: +- Docs: +- Source: - GPUI: - TanStack Query: diff --git a/crates/gpui-query/src/client/bucket/ops.rs b/crates/gpui-query/src/client/bucket/ops.rs index 382bd94..ad11e9f 100644 --- a/crates/gpui-query/src/client/bucket/ops.rs +++ b/crates/gpui-query/src/client/bucket/ops.rs @@ -56,9 +56,7 @@ impl QueryBu if entry.loading { return None; } - if entry.entity.upgrade().is_none() { - return None; - } + entry.entity.upgrade()?; Some((key, entry.last_updated_ms.unwrap_or(0))) }) .min_by_key(|&(_, age)| age); diff --git a/crates/gpui-query/src/client/infinite_bucket.rs b/crates/gpui-query/src/client/infinite_bucket.rs index 25b8881..923c178 100644 --- a/crates/gpui-query/src/client/infinite_bucket.rs +++ b/crates/gpui-query/src/client/infinite_bucket.rs @@ -114,9 +114,9 @@ impl Infinit /// Evict the oldest (least-recently-updated) entry to make room (#1 fix). /// /// **M2 (O(n)→O(1) entity reads)**: scans entry MIRRORS (no `entity.read`) - /// + `WeakEntity::upgrade` liveness, then one `entity.read` on the winner - /// to confirm `!is_loading()` (guards #109 against a stale mirror) and - /// read the authoritative timestamp. Mirrors + /// plus `WeakEntity::upgrade` liveness, then one `entity.read` on the + /// winner to confirm `!is_loading()` (guards #109 against a stale mirror) + /// and read the authoritative timestamp. Mirrors /// [`QueryBucket::evict_oldest`](super::QueryBucket::evict_oldest). pub(crate) fn evict_oldest(&mut self, cx: &App) { loop { @@ -127,9 +127,7 @@ impl Infinit if entry.loading { return None; } - if entry.entity.upgrade().is_none() { - return None; - } + entry.entity.upgrade()?; Some((key, entry.last_updated_ms.unwrap_or(0))) }) .min_by_key(|&(_, age)| age); diff --git a/crates/gpui-query/src/client/mod.rs b/crates/gpui-query/src/client/mod.rs index 5355964..1af652a 100644 --- a/crates/gpui-query/src/client/mod.rs +++ b/crates/gpui-query/src/client/mod.rs @@ -216,7 +216,7 @@ impl QueryClient { self.last_gc_ms = now_ms; return; } - if now_ms.saturating_sub(self.last_gc_ms) < MIN_GC_TIME_MS as u64 { + if now_ms.saturating_sub(self.last_gc_ms) < MIN_GC_TIME_MS { return; } self.last_gc_ms = now_ms; @@ -327,11 +327,11 @@ impl QueryClient { /// Downcast an erased query bucket to `&mut QueryBucket`, recreating /// it in place on the (impossible) type mismatch. /// - /// **M4**: this replaces the 5x duplicated `TypeId` pre-check + `eprintln` - /// + fresh-bucket + `downcast_mut` match block. `Any::downcast_mut` checks + /// **M4**: this replaces the 5x duplicated `TypeId` pre-check, `eprintln`, + /// fresh-bucket, and `downcast_mut` match block. `Any::downcast_mut` checks /// `TypeId` internally, so the explicit pre-check was redundant; we now /// downcast once and, only on the (impossible-after-construction) `None`, - /// log + swap in a fresh typed bucket and downcast *that* (which always + /// log, swap in a fresh typed bucket, and downcast *that* (which always /// succeeds). No production panic. fn bucket_or_recreate( bucket: &mut Box, diff --git a/crates/gpui-query/src/client/mutation_bucket.rs b/crates/gpui-query/src/client/mutation_bucket.rs index 310717a..662a27b 100644 --- a/crates/gpui-query/src/client/mutation_bucket.rs +++ b/crates/gpui-query/src/client/mutation_bucket.rs @@ -91,7 +91,6 @@ pub struct MutationBucket { /// (Audit #41) The previous private `now_ms()` duplicate is removed; this /// module now reuses the canonical [`super::erased::current_time_ms`]. - impl< V: Clone + Send + Sync + 'static, T: Clone + Send + Sync + 'static, @@ -109,7 +108,7 @@ impl< /// Evict the oldest (least-recently-updated) entry to make room for a new one. /// /// **M2 (O(n)→O(1) entity reads)**: scans entry MIRRORS (no `entity.read`) - /// + `WeakEntity::upgrade` liveness, picking the entry with the smallest + /// plus `WeakEntity::upgrade` liveness, picking the entry with the smallest /// mirrored recency — preferring the completion-time mirror /// (`last_updated_ms`) and falling back to the insertion time /// (`updated_at`) for never-completed mutations (audit #112). Then **one** @@ -127,9 +126,7 @@ impl< if entry.loading { return None; } - if entry.entity.upgrade().is_none() { - return None; - } + entry.entity.upgrade()?; // Prefer completion-time mirror; fall back to insertion // time for never-completed mutations (audit #112). let age = entry.last_updated_ms.unwrap_or(entry.updated_at); diff --git a/crates/gpui-query/src/client/prepared_fetch.rs b/crates/gpui-query/src/client/prepared_fetch.rs index 3596326..c1a6759 100644 --- a/crates/gpui-query/src/client/prepared_fetch.rs +++ b/crates/gpui-query/src/client/prepared_fetch.rs @@ -61,8 +61,7 @@ impl Prepare /// re-syscalling `current_time_ms()`. pub fn complete_success(self, data: T, cx: &mut App) { self.entity.update(cx, |resource, cx| { - let accepted = - resource.complete_current_success(self.request_id, data, self.now_ms); + let accepted = resource.complete_current_success(self.request_id, data, self.now_ms); // B2: precise dirty signal for the persistence layer. Imperative // completions (`prepare_fetch_query`) mutate the cache just like the // hook-layer completions, so they must wake `persist_with` too — @@ -86,8 +85,7 @@ impl Prepare /// re-syscalling `current_time_ms()`. pub fn complete_failure(self, error: E, cx: &mut App) { self.entity.update(cx, |resource, cx| { - let accepted = - resource.complete_current_failure(self.request_id, error, self.now_ms); + let accepted = resource.complete_current_failure(self.request_id, error, self.now_ms); // B2: see `complete_success` — bump only when the failure was // actually accepted, so an imperative failure is visible to // `persist_with` without spurious saves on stale completions. diff --git a/crates/gpui-query/src/tests/core_cache/mod.rs b/crates/gpui-query/src/tests/core_cache/mod.rs index 1b5e97a..94a9511 100644 --- a/crates/gpui-query/src/tests/core_cache/mod.rs +++ b/crates/gpui-query/src/tests/core_cache/mod.rs @@ -37,9 +37,9 @@ pub(crate) const STALE_MS: u64 = 2_000; pub(crate) const SWR_TOTAL_MS: u64 = TTL_MS + STALE_MS; // 3_000 // Derived boundary offsets from STORED_AT_MS: -pub(crate) const AT_TTL_BOUNDARY: u64 = STORED_AT_MS + TTL_MS as u64; // 2_000 — exactly at TTL edge +pub(crate) const AT_TTL_BOUNDARY: u64 = STORED_AT_MS + TTL_MS; // 2_000 — exactly at TTL edge pub(crate) const ONE_MS_PAST_TTL: u64 = AT_TTL_BOUNDARY + 1; // 2_001 — just past TTL -pub(crate) const AT_SWR_BOUNDARY: u64 = STORED_AT_MS + SWR_TOTAL_MS as u64; // 4_000 — exactly at total edge +pub(crate) const AT_SWR_BOUNDARY: u64 = STORED_AT_MS + SWR_TOTAL_MS; // 4_000 — exactly at total edge pub(crate) const ONE_MS_PAST_SWR: u64 = AT_SWR_BOUNDARY + 1; // 4_001 — fully expired // ── Helpers ────────────────────────────────────────────────────────────── diff --git a/crates/gpui-query/src/tests/core_infinite_query/max_pages.rs b/crates/gpui-query/src/tests/core_infinite_query/max_pages.rs index e2b8ee6..5cbaa7a 100644 --- a/crates/gpui-query/src/tests/core_infinite_query/max_pages.rs +++ b/crates/gpui-query/src/tests/core_infinite_query/max_pages.rs @@ -96,11 +96,11 @@ fn max_pages_50_allows_50_pages_and_evicts_on_51st() { ]; // Load 50 pages — all with has_more=true so has_next_page stays true - for i in 0..50 { + for (i, label) in P_LABELS.iter().enumerate().take(50) { let id = r.begin_fetch_next(&mut seq, (i * 100) as u64).unwrap(); r.complete_page_success( id, - vec![P_LABELS[i]], + vec![*label], true, // always report more pages available true, ((i + 1) * 100) as u64, diff --git a/crates/gpui-query/src/tests/integration_client_coverage/client_operations/persist_with_hydrate.rs b/crates/gpui-query/src/tests/integration_client_coverage/client_operations/persist_with_hydrate.rs index 43e16c6..e686b97 100644 --- a/crates/gpui-query/src/tests/integration_client_coverage/client_operations/persist_with_hydrate.rs +++ b/crates/gpui-query/src/tests/integration_client_coverage/client_operations/persist_with_hydrate.rs @@ -17,7 +17,7 @@ use crate::core::{ InfiniteQueryResource, MutationResource, QueryError, QueryKey, QueryResource, QueryStatus, }; use crate::hook::{ - fetch_query, mutate, use_infinite_query, use_mutation, use_query_manual, InfiniteQueryOptions, + InfiniteQueryOptions, fetch_query, mutate, use_infinite_query, use_mutation, use_query_manual, }; use crate::tests::test_support::*; @@ -168,7 +168,7 @@ fn test_persist_with_saves_on_mutation(cx: &mut TestAppContext) { // the snapshot. The harness holds it for the life of the test, mirroring a // real component holding the `Entity` from `use_query`. struct H { - entity: Entity>, + _entity: Entity>, _handle: PersistHandle, } let harness = cx.new(|cx| { @@ -195,7 +195,10 @@ fn test_persist_with_saves_on_mutation(cx: &mut TestAppContext) { }); (handle, entity) }); - H { entity, _handle } + H { + _entity: entity, + _handle, + } }); // A mutation (on another key) bumps the dirty signal → persist_with collects @@ -245,7 +248,7 @@ fn test_hydrate_primes_via_deserializer_registry(cx: &mut TestAppContext) { // harness; hydrate's `set_query_data` then reuses the retained entity rather // than creating one that is immediately dropped. struct H { - entity: Entity>, + _entity: Entity>, } let harness = cx.new(|cx| { cx.update_global::(|client, _cx| { @@ -255,7 +258,7 @@ fn test_hydrate_primes_via_deserializer_registry(cx: &mut TestAppContext) { let entity = cx.update_global::(|client, cx| { client.resource::(QueryKey::from("hydrate_k"), cx) }); - H { entity } + H { _entity: entity } }); // Run the async hydrate on the background executor, then assert priming. @@ -448,7 +451,7 @@ fn test_persist_with_driven_by_real_mutation_completion(cx: &mut TestAppContext) struct H { // A retained Success query entry — the thing actually persisted. - query: Entity>, + _query: Entity>, // The mutation entity; completing it bumps the dirty signal. mutation: Entity>, _handle: PersistHandle, @@ -479,7 +482,7 @@ fn test_persist_with_driven_by_real_mutation_completion(cx: &mut TestAppContext) // Create the mutation via the REAL hook path. let (mutation, _msub) = use_mutation::((), cx); H { - query, + _query: query, mutation, _handle, } @@ -563,10 +566,7 @@ fn test_persist_with_driven_by_real_infinite_completion(cx: &mut TestAppContext) |_last_page| async move { Ok::<_, QueryError>((vec!["page-0".to_string()], true)) }, cx, ); - H { - infinite, - _handle, - } + H { infinite, _handle } }); // Let the first-page fetch resolve → bumps `CacheMutation` → `persist_with` @@ -706,7 +706,7 @@ fn test_persist_with_debounce_coalesces(cx: &mut TestAppContext) { // Hold a live Success entry on the "coalesced" key so the snapshot actually // contains something to save (the bucket stores only WeakEntity). struct H { - entity: Entity>, + _entity: Entity>, _handle: PersistHandle, } let harness = cx.new(|cx| { @@ -728,7 +728,10 @@ fn test_persist_with_debounce_coalesces(cx: &mut TestAppContext) { }); (handle, entity) }); - H { entity, _handle } + H { + _entity: entity, + _handle, + } }); // Fire N>=3 rapid mutations on the same driver key. Each is issued in its @@ -758,7 +761,8 @@ fn test_persist_with_debounce_coalesces(cx: &mut TestAppContext) { // the pending timer tasks; a subsequent `run_until_parked` lets exactly one // of them drain the shared slot and run `save`. The remaining tasks wake to // find the slot already empty and no-op. - cx.background_executor.advance_clock(debounce + Duration::from_millis(1)); + cx.background_executor + .advance_clock(debounce + Duration::from_millis(1)); cx.run_until_parked(); assert_eq!( @@ -788,15 +792,9 @@ fn test_persist_with_debounce_coalesces(cx: &mut TestAppContext) { fn block_on_ready(fut: impl std::future::Future) -> R { use std::future::Future; use std::pin::Pin; - use std::sync::Arc; - use std::task::{Context, Poll, Wake, Waker}; + use std::task::{Context, Poll, Waker}; - struct NoWake; - impl Wake for NoWake { - fn wake(self: Arc) {} - } - let waker = Waker::from(Arc::new(NoWake)); - let mut cx = Context::from_waker(&waker); + let mut cx = Context::from_waker(Waker::noop()); let mut fut = Box::pin(fut); // SAFETY: pinned on the heap; we hold the only reference. let mut pinned: Pin<&mut dyn Future> = Pin::as_mut(&mut fut); diff --git a/crates/gpui-query/src/tests/property_tests/query_key/proptests.rs b/crates/gpui-query/src/tests/property_tests/query_key/proptests.rs index 7c072cb..044bd8c 100644 --- a/crates/gpui-query/src/tests/property_tests/query_key/proptests.rs +++ b/crates/gpui-query/src/tests/property_tests/query_key/proptests.rs @@ -15,9 +15,10 @@ use super::strategies::*; // default `cargo test` run cheap. The heavyweight long-key / very-long-string // invariants are also covered by the deterministic_tests module. fn test_config() -> ProptestConfig { - let mut config = ProptestConfig::default(); - config.cases = 64; - config + ProptestConfig { + cases: 64, + ..ProptestConfig::default() + } } // ── 1. Equality ───────────────────────────────────────────────────────── diff --git a/crates/gpui-query/src/tests/test_support.rs b/crates/gpui-query/src/tests/test_support.rs index f8c60a3..f4a85e7 100644 --- a/crates/gpui-query/src/tests/test_support.rs +++ b/crates/gpui-query/src/tests/test_support.rs @@ -118,6 +118,7 @@ pub fn test_sequencer() -> RequestSequencer { /// Uses `CachePolicy::NoCache` so every `begin_request` returns `Started` /// (never `CacheHit`), giving deterministic control over each fetch lifecycle /// step without worrying about TTL freshness windows. +#[expect(dead_code, reason = "kept as a shared lifecycle-test helper")] pub fn resource_with_sequencer( key: impl Into, ) -> (QueryResource<&'static str>, RequestSequencer) { diff --git a/justfile b/justfile index 784af72..a3b9c70 100644 --- a/justfile +++ b/justfile @@ -17,30 +17,24 @@ test: test-feature feature: cargo test --features "{{ feature }}" -# ---- Docs ---- - -# Build the Docusaurus docs site -docs-build: - cd website && npm run build - -# Start the docs dev server -docs-dev: - cd website && npm run start - -# ---- Website (TanStack Start) ---- +# ---- Website (Astro + Starlight) ---- # Install web dependencies web-install: cd web && bun install -# Build the web app (copies Docusaurus output into it) +# Build the website (marketing, blog, and /docs/** — output in dist/client) web-build: cd web && bun run build -# Start the web dev server +# Start the web dev server (Astro, port 3000) web-dev: cd web && bun run dev +# Preview the built site +web-preview: + cd web && bun run preview + # ---- CI Workflows ---- # Trigger a manual crate publish for a given tag (e.g. just publish v0.1.1) @@ -67,7 +61,7 @@ ci-logs run_id: # Open the GitHub releases page releases: - open "https://github.com/hmziqrs/gpui-query/releases" + open "https://github.com/freeoxide/gpui-query/releases" # Create a new release: bumps version in CHANGELOG, commits, and pushes # Usage: just release 0.1.2 diff --git a/logo-variants/logo-black-on-white-1024.png b/logo-variants/logo-black-on-white-1024.png new file mode 100644 index 0000000..b6797cb Binary files /dev/null and b/logo-variants/logo-black-on-white-1024.png differ diff --git a/logo-variants/logo-black-transparent-1024.png b/logo-variants/logo-black-transparent-1024.png new file mode 100644 index 0000000..5971bb7 Binary files /dev/null and b/logo-variants/logo-black-transparent-1024.png differ diff --git a/logo-variants/logo-black-transparent-256.png b/logo-variants/logo-black-transparent-256.png new file mode 100644 index 0000000..1ee0b60 Binary files /dev/null and b/logo-variants/logo-black-transparent-256.png differ diff --git a/logo-variants/logo-black-transparent-512.png b/logo-variants/logo-black-transparent-512.png new file mode 100644 index 0000000..4ca22c8 Binary files /dev/null and b/logo-variants/logo-black-transparent-512.png differ diff --git a/logo-variants/logo-lime-dark-transparent-512.png b/logo-variants/logo-lime-dark-transparent-512.png new file mode 100644 index 0000000..e8c583d Binary files /dev/null and b/logo-variants/logo-lime-dark-transparent-512.png differ diff --git a/logo-variants/logo-lime-on-dark-1024.png b/logo-variants/logo-lime-on-dark-1024.png new file mode 100644 index 0000000..68fc293 Binary files /dev/null and b/logo-variants/logo-lime-on-dark-1024.png differ diff --git a/logo-variants/logo-lime-on-light-1024.png b/logo-variants/logo-lime-on-light-1024.png new file mode 100644 index 0000000..6befe0a Binary files /dev/null and b/logo-variants/logo-lime-on-light-1024.png differ diff --git a/logo-variants/logo-lime-transparent-1024.png b/logo-variants/logo-lime-transparent-1024.png new file mode 100644 index 0000000..082b3fe Binary files /dev/null and b/logo-variants/logo-lime-transparent-1024.png differ diff --git a/logo-variants/logo-lime-transparent-256.png b/logo-variants/logo-lime-transparent-256.png new file mode 100644 index 0000000..f59fdfe Binary files /dev/null and b/logo-variants/logo-lime-transparent-256.png differ diff --git a/logo-variants/logo-lime-transparent-512.png b/logo-variants/logo-lime-transparent-512.png new file mode 100644 index 0000000..4da6a2f Binary files /dev/null and b/logo-variants/logo-lime-transparent-512.png differ diff --git a/logo-variants/logo-white-on-dark-1024.png b/logo-variants/logo-white-on-dark-1024.png new file mode 100644 index 0000000..1e742aa Binary files /dev/null and b/logo-variants/logo-white-on-dark-1024.png differ diff --git a/logo-variants/logo-white-transparent-1024.png b/logo-variants/logo-white-transparent-1024.png new file mode 100644 index 0000000..745d094 Binary files /dev/null and b/logo-variants/logo-white-transparent-1024.png differ diff --git a/logo-variants/logo-white-transparent-512.png b/logo-variants/logo-white-transparent-512.png new file mode 100644 index 0000000..82d4ef3 Binary files /dev/null and b/logo-variants/logo-white-transparent-512.png differ diff --git a/shared/tokens.css b/shared/tokens.css index 2dd205e..664ca68 100644 --- a/shared/tokens.css +++ b/shared/tokens.css @@ -1,14 +1,12 @@ /* ========================================================================== gpui-query — Shared Design Tokens Single source of truth for colors, typography, spacing, and radius. - Used by both web/ (TanStack Start) and website/ (Docusaurus). + Consumed by web/ (Astro marketing + Starlight docs). Fonts are self-hosted + via @fontsource-variable/* in the app/styles and docs stylesheets, so no + Google Fonts @import is needed here (it previously caused an out-of-order + @import warning and a double font payload). ========================================================================== */ -/* -------------------------------------------------------------------------- - Google Fonts - -------------------------------------------------------------------------- */ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap'); - /* -------------------------------------------------------------------------- Light Mode -------------------------------------------------------------------------- */ @@ -65,9 +63,9 @@ [data-theme='dark'] { --color-background: oklch(0.145 0 0); --color-foreground: oklch(0.985 0 0); - --color-card: oklch(0.205 0 0); + --color-card: oklch(0.165 0 0); --color-card-foreground: oklch(0.985 0 0); - --color-popover: oklch(0.205 0 0); + --color-popover: oklch(0.165 0 0); --color-popover-foreground: oklch(0.985 0 0); --color-primary: oklch(0.768 0.233 130.85); --color-primary-foreground: oklch(0.405 0.101 131.063); diff --git a/web/.gitignore b/web/.gitignore index 9b072d2..41ae9ed 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -6,6 +6,7 @@ dist-ssr .env .nitro .tanstack +.astro .wrangler .output .vinxi diff --git a/web/.vite-hooks/pre-commit b/web/.vite-hooks/pre-commit deleted file mode 100755 index a89414d..0000000 --- a/web/.vite-hooks/pre-commit +++ /dev/null @@ -1 +0,0 @@ -cd web && vp staged diff --git a/web/AGENTS.md b/web/AGENTS.md index 362b82a..a1bbf4e 100644 --- a/web/AGENTS.md +++ b/web/AGENTS.md @@ -1,16 +1,34 @@ - +# Website (Astro + Starlight) -# Using Vite+, the Unified Toolchain for the Web +This is the gpui-query website: a static Astro + Starlight site deployed to +Cloudflare Pages. The migration from TanStack Start + Docusaurus is complete — +do **not** reintroduce Vite+, TanStack Start, or Docusaurus. -This project is using Vite+, a unified toolchain built on top of Vite, Rolldown, Vitest, tsdown, Oxlint, Oxfmt, and Vite Task. Vite+ wraps runtime management, package management, and frontend tooling in a single global CLI called `vp`. Vite+ is distinct from Vite, and it invokes Vite through `vp dev` and `vp build`. Run `vp help` to print a list of commands and `vp --help` for information about a specific command. +## Commands (run from `web/`) -Docs are local at `node_modules/vite-plus/docs` or online at https://viteplus.dev/guide/. +- `bun install` — install dependencies. +- `bun run dev` — Astro dev server on http://localhost:3000. +- `bun run build` — full production build: OG images → `astro build` → Pagefind + index → `llms.txt` / `.md` alternatives. Output is `dist/client/`. +- `bun run preview` — serve the built site. +- `astro check` — type-check `.astro` / `.tsx` sources. -## Review Checklist +## Layout -- [ ] Run `vp install` after pulling remote changes and before getting started. -- [ ] Run `vp check` and `vp test` to format, lint, type check and test changes. -- [ ] Check if there are `vite.config.ts` tasks or `package.json` scripts necessary for validation, run via `vp run + + + Skip to content +
+ +
+ +
+
+
+ + + diff --git a/web/src/lib/blog.ts b/web/src/lib/blog.ts deleted file mode 100644 index d09ac1d..0000000 --- a/web/src/lib/blog.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { type ComponentType } from "react"; - -import IntroducingGpuiQueryPost from "../content/blog/introducing-gpui-query"; -import WhyGpuiQueryPost from "../content/blog/why-gpui-query"; -import CooperativeCancellationPost from "../content/blog/cooperative-cancellation"; -import CachePoliciesExplainedPost from "../content/blog/cache-policies-explained"; - -/* ─── Frontmatter schema ────────────────────────────────────────────── */ - -export interface BlogFrontmatter { - title: string; - description: string; - /** ISO date, YYYY-MM-DD */ - date: string; - author: string; - tags?: string[]; -} - -export interface BlogPost { - slug: string; - Content: ComponentType; - frontmatter: BlogFrontmatter; -} - -/* ─── Post registry ─────────────────────────────────────────────────── */ - -/** - * Static post registry. Blog bodies are authored as TSX components (see - * `src/content/blog/*.tsx`) so they prerender cleanly under TanStack Start's - * SSR environment. MDX + `import.meta.glob` did not resolve module exports - * during prerender, leaving posts empty — TSX components do not have that - * problem. Frontmatter lives here alongside the component import. - */ -const POSTS: BlogPost[] = [ - { - slug: "introducing-gpui-query", - Content: IntroducingGpuiQueryPost, - frontmatter: { - title: "Introducing gpui-query", - description: - "Zero-boilerplate async state management for GPUI — inspired by TanStack Query, built for Rust.", - date: "2026-03-10", - author: "hmziqrs", - tags: ["gpui", "rust", "async", "introduction"], - }, - }, - { - slug: "why-gpui-query", - Content: WhyGpuiQueryPost, - frontmatter: { - title: "Why gpui-query", - description: - "Bringing TanStack Query's battle-tested async state patterns to GPUI and Rust — without the boilerplate.", - date: "2026-03-12", - author: "hmziqrs", - tags: ["gpui", "rust", "async", "introduction"], - }, - }, - { - slug: "cooperative-cancellation", - Content: CooperativeCancellationPost, - frontmatter: { - title: "Cooperative Cancellation in gpui-query", - description: - "How QuerySignal uses Arc to cancel in-flight queries cleanly when components unmount.", - date: "2026-04-08", - author: "hmziqrs", - tags: ["gpui", "rust", "concurrency", "cancellation"], - }, - }, - { - slug: "cache-policies-explained", - Content: CachePoliciesExplainedPost, - frontmatter: { - title: "Cache Policies, Explained", - description: - "NoCache vs Ttl vs StaleWhileRevalidate — when each CachePolicy variant is the right choice, and how they interact with retries and observers.", - date: "2026-05-02", - author: "hmziqrs", - tags: ["gpui", "rust", "caching", "performance"], - }, - }, -]; - -/** All blog posts, sorted by date descending. */ -export function getAllBlogPosts(): BlogPost[] { - return [...POSTS].sort( - (a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime(), - ); -} - -/** Look up a single post by its slug (filename without extension). */ -export function getBlogBySlug(slug: string): BlogPost | undefined { - return POSTS.find((post) => post.slug === slug); -} - -/** Slugs only — handy for prerendering and feeds. */ -export function getBlogSlugs(): string[] { - return POSTS.map((post) => post.slug); -} diff --git a/web/src/lib/firebase.ts b/web/src/lib/firebase.ts new file mode 100644 index 0000000..ceeeb54 --- /dev/null +++ b/web/src/lib/firebase.ts @@ -0,0 +1,60 @@ +import { initializeApp } from "firebase/app"; +import { + initializeAnalytics, + isSupported, + logEvent, + type Analytics, +} from "firebase/analytics"; + +const firebaseConfig = { + apiKey: "AIzaSyCux3FsCYpaoEyhKoLQQ-9X3NUxao6Uj1U", + authDomain: "gpui-query.firebaseapp.com", + projectId: "gpui-query", + storageBucket: "gpui-query.firebasestorage.app", + messagingSenderId: "260261054618", + appId: "1:260261054618:web:8bbc954396f4cbac523723", + measurementId: "G-V2TY39PKSH", +}; + +const app = initializeApp(firebaseConfig); +let analyticsPromise: Promise | undefined; +let lastPageLocation: string | undefined; + +async function getFirebaseAnalytics() { + if (typeof window === "undefined") { + return null; + } + + analyticsPromise ??= isSupported() + .then((supported) => + supported + ? initializeAnalytics(app, { + config: { + send_page_view: false, + }, + }) + : null, + ) + .catch(() => null); + + return analyticsPromise; +} + +export async function logFirebasePageView() { + const analytics = await getFirebaseAnalytics(); + if (!analytics || typeof window === "undefined") { + return; + } + + const pageLocation = window.location.href; + if (pageLocation === lastPageLocation) { + return; + } + + lastPageLocation = pageLocation; + logEvent(analytics, "page_view", { + page_title: document.title, + page_location: pageLocation, + page_path: `${window.location.pathname}${window.location.search}`, + }); +} diff --git a/web/src/lib/flags.ts b/web/src/lib/flags.ts new file mode 100644 index 0000000..5553478 --- /dev/null +++ b/web/src/lib/flags.ts @@ -0,0 +1,9 @@ +/** + * Master switch for the /v1–/v4 landing design previews. While false, the + * preview routes throw notFound(), they are skipped at prerender time (see + * vite.config.ts), and the floating VersionSwitcher renders nothing. Flip to + * true to bring the previews back while iterating on landing designs. + * + * V4 won and is rendered at `/` — see src/routes/index.tsx. + */ +export const LANDING_PREVIEWS_ENABLED = false; diff --git a/web/src/lib/release.ts b/web/src/lib/release.ts new file mode 100644 index 0000000..e058b4c --- /dev/null +++ b/web/src/lib/release.ts @@ -0,0 +1,125 @@ +/** + * Build-time source of truth for the crate version and changelog. + * + * The version lives in `crates/gpui-query/Cargo.toml`; the changelog lives in + * the repo-root `CHANGELOG.md`. This module reads both at build time so the + * website never hardcodes a duplicate that can drift. + * + * Paths resolve from `process.cwd()`, which is `web/` for both consumers — + * the `astro build` (SSG) and the bare-`node` prebuild scripts (the OG script + * already relies on this, e.g. `resolve("src/content/blog")`). We deliberately + * do NOT use `import.meta.url`: Astro bundles this module into a prerender + * chunk under `dist/`, where `import.meta.url` points at the built chunk, not + * the source file. + */ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +const REPO_ROOT = resolve(process.cwd(), ".."); + +export type Category = "Added" | "Changed" | "Fixed" | "Removed"; + +export interface ChangelogItem { + category: Category; + text: string; +} + +export interface ChangelogEntry { + /** Version without a leading "v", e.g. "0.2.0". */ + version: string; + /** ISO date, e.g. "2026-07-21". */ + date: string; + /** One-line summary from the blockquote under the version header, if any. */ + description?: string; + items: ChangelogItem[]; +} + +/** + * Read the main crate's version from its Cargo manifest. This is the canonical + * version number; the release workflow keeps it in sync with `CHANGELOG.md`. + */ +export function crateVersion(): string { + const toml = readFileSync( + resolve(REPO_ROOT, "crates/gpui-query/Cargo.toml"), + "utf8", + ); + const m = toml.match(/^version = "([^"]+)"/m); + return m ? m[1] : "0.0.0"; +} + +const VERSION_HEADER = /^## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2})/; +const CATEGORY = /^### (Added|Changed|Fixed|Removed)\b/; +const BLOCKQUOTE = /^>\s?(.*)$/; +const ITEM = /^[-*]\s+(.+)$/; + +/** + * Parse the repo-root `CHANGELOG.md` (Keep a Changelog format) into entries. + * + * Each `## [x.y.z] - YYYY-MM-DD` header starts an entry. An optional blockquote + * line (`> summary`) before the first `### ` becomes the `description`. + * `### Added|Changed|Fixed|Removed` sections collect their `- ` bullets as + * items. `####` sub-headings, blank lines, and the trailing link-reference + * footer are ignored. Versions without a date (e.g. an `## [Unreleased]`) + * are skipped. + */ +export function parseChangelog(): ChangelogEntry[] { + const md = readFileSync(resolve(REPO_ROOT, "CHANGELOG.md"), "utf8"); + const lines = md.split(/\r?\n/); + const entries: ChangelogEntry[] = []; + let cur: ChangelogEntry | null = null; + let category: Category | null = null; + let descriptionDone = false; + + for (const line of lines) { + const head = line.match(VERSION_HEADER); + if (head) { + if (cur) entries.push(cur); + cur = { version: head[1], date: head[2], items: [] }; + category = null; + descriptionDone = false; + continue; + } + if (!cur) continue; // preamble / link footer before the first version + if (/^## /.test(line)) { + // A non-dated `##` header (e.g. Unreleased): close the current entry. + entries.push(cur); + cur = null; + continue; + } + const cat = line.match(CATEGORY); + if (cat) { + category = cat[1] as Category; + descriptionDone = true; + continue; + } + if (!descriptionDone) { + const bq = line.match(BLOCKQUOTE); + if (bq && bq[1].trim()) { + cur.description = bq[1].trim(); + descriptionDone = true; + continue; + } + } + const item = line.match(ITEM); + if (item && category) { + cur.items.push({ category, text: item[1].trim() }); + } + } + if (cur) entries.push(cur); + return entries; +} + +/** + * The latest release for OG/SEO use: the version from Cargo.toml paired with + * the date of the top changelog entry. Returns `undefined` if either source + * can't be read. + */ +export function latestRelease(): { version: string; date: string } | undefined { + try { + const entries = parseChangelog(); + if (entries.length === 0) return undefined; + return { version: crateVersion(), date: entries[0].date }; + } catch { + return undefined; + } +} diff --git a/web/src/lib/seo.ts b/web/src/lib/seo.ts index 11dcdeb..2b0a2f9 100644 --- a/web/src/lib/seo.ts +++ b/web/src/lib/seo.ts @@ -57,11 +57,10 @@ export function softwareSourceCode(data: SoftwareSourceCodeData) { } /* - * The following TechArticle / HowTo / BlogPosting factories are intentionally - * retained even though some have no current call site in the marketing app: - * the docs (Docusaurus) and blog moved out of web/, but the blog will be - * re-introduced and wired to blogPost(). techArticle() and howTo() are kept - * for parity and future doc/blog embeds. Do not remove. + * blogPost() is wired to /blog/$slug and changelogPage() to /changelog. + * techArticle() and howTo() have no current call site: they are retained for + * future doc/blog embeds (the docs live in the separate Docusaurus app). Do + * not remove. */ // Retained for Docusaurus/future use — no current call site in this app. export function techArticle(data: TechArticleData) { @@ -114,7 +113,7 @@ export function blogPost(data: BlogPostData) { description: data.description, ...(data.author && { author: { "@type": "Person", name: data.author } }), ...(data.datePublished && { datePublished: data.datePublished }), - ...(data.url && { url: data.url }), + ...(data.url && { url: data.url, mainEntityOfPage: data.url }), ...(data.image && { image: data.image }), }; } diff --git a/web/src/pages/404.astro b/web/src/pages/404.astro new file mode 100644 index 0000000..4aa8609 --- /dev/null +++ b/web/src/pages/404.astro @@ -0,0 +1,36 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +// Starlight is configured with disable404Route: true, so this page owns the +// site-wide 404. (Ported from the NotFoundPage in routes/__root.tsx.) +--- + +
+
+

404

+

Page not found

+

+ The page you are looking for does not exist or has been moved. +

+ +
+
+
diff --git a/web/src/pages/blog.astro b/web/src/pages/blog.astro new file mode 100644 index 0000000..4e19a4c --- /dev/null +++ b/web/src/pages/blog.astro @@ -0,0 +1,88 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +import { getCollection } from "astro:content"; + +// Replaces the frontmatter-only glob in lib/blog.ts with an Astro content +// collection query. Posts are sorted newest-first (same as getAllBlogPosts). +const posts = (await getCollection("blog")).sort( + (a, b) => b.data.date.getTime() - a.data.date.getTime() +); + +const title = "Blog - gpui-query"; +const description = + "Deep dives on async state management for GPUI in Rust: cache policies, cooperative cancellation, and updates from the gpui-query project."; +const jsonLd = { + "@context": "https://schema.org", + "@type": "Blog", + name: "gpui-query Blog", + url: "https://gpui-query.freeoxide.com/blog", + description, + blogPost: posts.map((post) => ({ + "@type": "BlogPosting", + headline: post.data.title, + description: post.data.description, + datePublished: post.data.date.toISOString(), + author: { "@type": "Person", name: post.data.author }, + url: `https://gpui-query.freeoxide.com/blog/${post.id}`, + image: `https://gpui-query.freeoxide.com/og/blog/${post.id}.png`, + })), +}; + +function formatDate(d: Date): string { + return d.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" }); +} +--- + +
+
+
+

Blog

+

+ Announcements, deep dives, and updates about gpui-query. +

+
+ + {posts.length === 0 ? ( +
+

No posts yet. Check back soon!

+
+ ) : ( +
+ {posts.map((post, index) => ( +
+
+ + + + + + + {post.data.author} + + {index === 0 && ( + Latest + )} +
+ +

+ {post.data.title} +

+ +

{post.data.description}

+ + +
+ ))} +
+ )} +
+
+
diff --git a/web/src/pages/blog/[slug].astro b/web/src/pages/blog/[slug].astro new file mode 100644 index 0000000..8dc1bed --- /dev/null +++ b/web/src/pages/blog/[slug].astro @@ -0,0 +1,105 @@ +--- +import BaseLayout from "../../layouts/BaseLayout.astro"; +import { getCollection, render, type CollectionEntry } from "astro:content"; +import { blogPost } from "../../lib/seo"; + +type Props = { + post: CollectionEntry<"blog">; + newer?: CollectionEntry<"blog">; + older?: CollectionEntry<"blog">; +}; + +// Enumerate /blog/$slug from the collection; compute prev/next (newest-first). +export async function getStaticPaths() { + const all = (await getCollection("blog")).sort( + (a, b) => b.data.date.getTime() - a.data.date.getTime() + ); + return all.map((post, index) => ({ + params: { slug: post.id }, + props: { post, newer: all[index - 1], older: all[index + 1] }, + })); +} + +const { post, newer, older } = Astro.props; +const { Content } = await render(post); +const fm = post.data; + +const SITE = "https://gpui-query.freeoxide.com"; +const url = `${SITE}/blog/${post.id}`; +const ogImage = `${SITE}/og/blog/${post.id}.png`; +const title = `${fm.title} - gpui-query Blog`; +const jsonLd = blogPost({ + headline: fm.title, + description: fm.description, + author: fm.author, + datePublished: fm.date.toISOString(), + url, + image: ogImage, +}); + +function formatDate(d: Date): string { + return d.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" }); +} +--- + +
+ + + Back to blog + + +
+

{fm.title}

+

{fm.description}

+
+ {fm.author} + + +
+ {fm.tags && fm.tags.length > 0 && ( +
+ {fm.tags.map((tag) => ( + {tag} + ))} +
+ )} +
+ +
+ +
+ + +
+
diff --git a/web/src/pages/changelog.astro b/web/src/pages/changelog.astro new file mode 100644 index 0000000..0491c84 --- /dev/null +++ b/web/src/pages/changelog.astro @@ -0,0 +1,147 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +import { changelogPage } from "../lib/seo"; +import { parseChangelog, type Category } from "../lib/release"; + +// Generated at build time from the repo-root CHANGELOG.md (the single source of +// truth) via src/lib/release.ts. The per-entry headline comes from a blockquote +// summary under each version header in CHANGELOG.md; the doc cross-links below +// are web-only and have no CHANGELOG representation. +interface ChangelogSubItem { + category: Category; + text: string; +} +interface ChangelogLink { + label: string; + slug: string; +} +interface ChangelogEntry { + version: string; + date: string; + description?: string; + items: ChangelogSubItem[]; + links?: ChangelogLink[]; +} + +const categoryConfig: Record = { + Added: { bullet: "+", color: "text-emerald-600 dark:text-emerald-400" }, + Changed: { bullet: "~", color: "text-amber-600 dark:text-amber-400" }, + Fixed: { bullet: "!", color: "text-sky-600 dark:text-sky-400" }, + Removed: { bullet: "-", color: "text-rose-600 dark:text-rose-400" }, +}; + +// Web-only doc cross-links, keyed by version (no leading "v"). CHANGELOG.md has +// no notion of these, so they live here as a small sidecar. +const docLinks: Record = { + "0.1.3": [{ label: "Migrating from v1 to v2", slug: "/docs/advanced/migration" }], + "0.1.1": [{ label: "Migrating from v1 to v2", slug: "/docs/advanced/migration" }], + "0.1.0": [ + { label: "Getting Started", slug: "/docs/getting-started/installation" }, + { label: "Queries", slug: "/docs/api/queries" }, + ], +}; + +const changelogEntries: ChangelogEntry[] = parseChangelog().map((entry) => ({ + ...entry, + links: docLinks[entry.version], +})); + +const title = "Changelog - gpui-query"; +const description = + "gpui-query release history: the v1 to v2 rewrite, crate reorganization, fixes, and docs updates in every published version."; +const jsonLd = changelogPage({ + name: "gpui-query Changelog", + description: "Release history for gpui-query, the async state management library for GPUI.", + url: "https://gpui-query.freeoxide.com/changelog", + softwareVersion: changelogEntries[0].version, + datePublished: changelogEntries[changelogEntries.length - 1].date, + dateModified: changelogEntries[0].date, +}); + +function badgeClasses(index: number): string { + if (index === 0) return "border-transparent bg-primary text-primary-foreground shadow-sm"; + if (index === 1) return "border-transparent bg-secondary text-secondary-foreground"; + return "border-transparent bg-muted text-muted-foreground"; +} +--- + +
+
+
+

Changelog

+

+ Release history for gpui-query. Every version, every improvement. +

+
+ +
+ + +
+ {changelogEntries.map((entry, index) => ( +
+ + +
+
+ {`v${entry.version}`} + {index === 0 && ( + Latest + )} + +
+ + {entry.description &&

{entry.description}

} + + {entry.items.length > 0 && ( +
+ {entry.items.map((item) => { + const config = categoryConfig[item.category]; + return ( +
+ + + {item.category} + {item.text} + +
+ ); + })} +
+ )} + + {entry.links && entry.links.length > 0 && ( +
+ Docs + {entry.links.map((link) => ( + + {link.label} + + + ))} +
+ )} +
+
+ ))} +
+
+
+
+
diff --git a/web/src/pages/faq.astro b/web/src/pages/faq.astro new file mode 100644 index 0000000..570f248 --- /dev/null +++ b/web/src/pages/faq.astro @@ -0,0 +1,213 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +import { faqPage } from "../lib/seo"; + +// Native
/ instead of a React accordion: the answers stay in +// the DOM (crawlable for the FAQPage structured data) with no JS. Same content +// as the old faq.tsx. See astro-migration.mdx § "Route and content mapping". +interface FaqItem { + question: string; + answer: string; +} +interface FaqCategory { + label: string; + icon: string; // inner SVG markup (lucide-style) + items: FaqItem[]; +} + +const categories: FaqCategory[] = [ + { + label: "Getting Started", + icon: '', + items: [ + { + question: "How is gpui-query different from TanStack Query?", + answer: + "gpui-query adapts TanStack Query's patterns to Rust and the GPUI framework. It uses Rust's type system for compile-time guarantees, Arc for cooperative cancellation, and integrates directly with GPUI's render loop.", + }, + { + question: "Can I use gpui-query outside of Zed?", + answer: + "gpui-query is designed for the GPUI framework, which powers the Zed editor. While architecturally the Core layer is framework-agnostic, the Hook layer depends on GPUI's reactive primitives.", + }, + { + question: "How do I set up QueryClient in my app?", + answer: + "Create a QueryClient instance and register it in your GPUI application. The client manages all query resources, caching, and garbage collection. See the Getting Started guide for a complete walkthrough.", + }, + ], + }, + { + label: "Architecture", + icon: '', + items: [ + { + question: "Why does use_query return a tuple instead of an object?", + answer: + "use_query returns (Entity>, Subscription). Read data and status from the resource entity during render, and store the Subscription to keep the observation alive: dropping it stops updates, which is GPUI's standard lifecycle convention.", + }, + { + question: "What happens if my component unmounts during a fetch?", + answer: + "gpui-query uses cooperative cancellation via QuerySignal (Arc). When a component unmounts, the signal is set and the query checks it between retry attempts, which keeps teardown clean.", + }, + { + question: "What is QuerySignal and when do I check it?", + answer: + "QuerySignal is an Arc that enables cooperative cancellation. Long-running queries should check the signal periodically (especially between retry attempts) and abort early if cancelled.", + }, + ], + }, + { + label: "Advanced", + icon: '', + items: [ + { + question: "Why does LatestWins cancel my in-flight request?", + answer: + "LatestWins is a RequestPolicy that ensures only the most recent request's result is used. When a new request arrives, previous in-flight requests are cancelled via their signals, which prevents stale data from overwriting fresh results.", + }, + { + question: "How do I persist my query cache?", + answer: + "Enable the persist feature and implement the async Persister trait to save and restore query state across restarts. gpui-query supports custom backends (files, databases, KV) and ships a ready-made disk adapter in the gpui-query-persist crate.", + }, + { + question: "How do I handle pagination?", + answer: + "Use use_infinite_query for paginated data. It supports bidirectional fetching (fetch_next_page_infinite / fetch_previous_page_infinite) and configurable max_pages to limit cached pages.", + }, + { + question: "Is there a devtools experience?", + answer: + "gpui-query provides ClientDiagnostic types for inspecting cache state, query status, and resource lifecycle, a developer toolkit for debugging async state.", + }, + ], + }, +]; + +const faqItems: FaqItem[] = categories.flatMap((c) => c.items); +const title = "FAQ - gpui-query"; +const description = + "Common gpui-query questions answered: differences from TanStack Query, QueryClient setup, QuerySignal cancellation, persistence, and pagination."; +const jsonLd = faqPage({ + questions: faqItems.map((item) => ({ question: item.question, answer: item.answer })), +}); +const ogImage = "/og/faq.png"; +--- + +
+
+
+

Frequently Asked Questions

+

+ Common questions about gpui-query, grouped by topic. +

+
+ +
+ {categories.map((category) => ( +
+
+
+ +
+

{category.label}

+
+ +
+ {category.items.map((item) => ( +
+ + {item.question} + + +

{item.answer}

+
+ ))} +
+
+ ))} +
+ +
+

Still have questions?

+

+ Open an issue on GitHub and we will get back to you. Contributions and discussions are + always welcome. +

+ +
+
+
+
diff --git a/web/src/pages/index.astro b/web/src/pages/index.astro new file mode 100644 index 0000000..e392a5c --- /dev/null +++ b/web/src/pages/index.astro @@ -0,0 +1,24 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +import { LandingV4 } from "../components/landing/v4"; +import { softwareSourceCode } from "../lib/seo"; + +// V4 won the landing preview bake-off and renders as the homepage. The whole +// landing tree is one React island (state, timers, mouse tracking, scroll +// reveal via src/hooks/use-scroll-reveal.ts) — hydrated eagerly with client:load +// since the hero is above the fold. The /v1–/v4 preview routes stay disabled. +const title = "gpui-query: Async State Management for GPUI"; +const description = + "Zero-boilerplate async state management for GPUI. Caching, retry, cooperative cancellation, and persistence for the Zed editor's framework."; +const jsonLd = softwareSourceCode({ + name: "gpui-query", + description: "Zero-boilerplate async state management for GPUI", + programmingLanguage: "Rust", + codeRepository: "https://github.com/freeoxide/gpui-query", + url: "https://gpui-query.freeoxide.com", + license: "MIT", +}); +--- + + + diff --git a/web/src/pages/privacy.astro b/web/src/pages/privacy.astro new file mode 100644 index 0000000..ea25b93 --- /dev/null +++ b/web/src/pages/privacy.astro @@ -0,0 +1,112 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +import LegalPage from "../components/LegalPage.astro"; +import LegalSection from "../components/LegalSection.astro"; + +const title = "Privacy Policy - gpui-query"; +const description = + "How the gpui-query website handles data: Firebase Analytics, Google Fonts, Cloudflare hosting, and local storage."; +--- + + + +

+ This policy covers the gpui-query website at + gpui-query.freeoxide.com, including the + documentation and blog. The site has no user accounts, no comment forms, and no newsletter. + We do not ask you for personal information anywhere on it. The data described below is what + the services we build on collect. +

+
+ + +

+ We use Firebase Analytics (part of Google Firebase, operated by Google LLC) + to understand how the site is used: which pages are visited, roughly where visitors come + from, and what kind of device and browser they use. Firebase collects this through a device + identifier and reports it to us only in aggregate. We use it only to see which docs pages + get traffic and where readers drop off. +

+

+ Google's own use of this data is described in the + Firebase privacy documentation and the + Google Privacy Policy. Most content blockers block Firebase Analytics; the site works exactly the same with it + blocked. +

+
+ + +

+ The site is served by Cloudflare. Like any web host, Cloudflare sees your IP + address and request details and keeps short-lived server logs for security and operations. + See the + Cloudflare Privacy Policy. +

+
+ + +

+ Fonts are loaded from Google Fonts. When your browser fetches a font file, + your IP address is sent to Google as part of that request. Details are in Google's + Google Fonts privacy FAQ. +

+
+ + +

+ Your light/dark theme choice is saved in your browser's localStorage. It never + leaves your device and you can clear it at any time through your browser settings. +

+
+ + +

+ The site links out to GitHub, crates.io, docs.rs, and other third-party sites. Once you + follow one of those links, that site's privacy policy applies, not this one. +

+
+ + +

+ The site is developer documentation and is not directed to children under 13. We do not + knowingly collect any information from them. +

+
+ + +

+ If we change what the site collects (for example by adding or removing a service), we will + update this page and the date at the top. +

+
+ + +

+ Questions about this policy? Open an issue on + GitHub or email hmziqrs@gmail.com. +

+
+
+
diff --git a/web/src/pages/terms.astro b/web/src/pages/terms.astro new file mode 100644 index 0000000..3be02d6 --- /dev/null +++ b/web/src/pages/terms.astro @@ -0,0 +1,91 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +import LegalPage from "../components/LegalPage.astro"; +import LegalSection from "../components/LegalSection.astro"; + +const title = "Terms of Service - gpui-query"; +const description = + "Terms for using the gpui-query website and documentation. The gpui-query library itself is MIT licensed."; +--- + + + +

+ These terms apply to the gpui-query website at + gpui-query.freeoxide.com, which includes the + landing pages, documentation, blog, and changelog. By using the site you agree to them. They + are short because the site is simple: it is free documentation for an open-source library. +

+
+ + +

+ The gpui-query crate itself is distributed under the + MIT License. That license, not these terms, governs your use of the source code and the published + crate. You can use it in personal and commercial projects, modify it, and redistribute it, + subject to the license's conditions. +

+
+ + +

+ You may read, link to, and quote the documentation and blog posts with attribution. Code + snippets shown in the docs and blog are provided so you can use them; treat them as + MIT-licensed like the library they document. +

+
+ + +

+ The site and its content are provided as-is. We work to keep the documentation accurate, but + APIs change between releases and pages can lag behind the code. Nothing on this site is a + guarantee that the library is fit for a particular purpose, and we are not liable for damages + arising from your use of the site or the library (the same disclaimer the MIT License makes + for the code). +

+
+ + +

+ Don't attempt to disrupt the site, scrape it at a rate that degrades it for others, or use it + to distribute malware or spam. That's it. +

+
+ + +

+ The site links to third-party sites (GitHub, crates.io, docs.rs, and others) and relies on + the services described in the Privacy Policy, including Firebase + Analytics, Google Fonts, and Cloudflare. We don't control those services and aren't + responsible for their content or conduct. +

+
+ + +

+ We may update these terms as the site evolves. Material changes will be reflected on this + page with a new date at the top. Continuing to use the site after a change means you accept + the updated terms. +

+
+ + +

+ Questions about these terms? Open an issue on + GitHub or email hmziqrs@gmail.com. +

+
+
+
diff --git a/web/src/router.tsx b/web/src/router.tsx deleted file mode 100644 index 8ceb1ca..0000000 --- a/web/src/router.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { createRouter as createTanStackRouter } from "@tanstack/react-router"; -import { routeTree } from "./routeTree.gen"; - -export function getRouter() { - const router = createTanStackRouter({ - routeTree, - scrollRestoration: true, - defaultPreload: "intent", - defaultPreloadStaleTime: 0, - }); - - return router; -} - -declare module "@tanstack/react-router" { - interface Register { - router: ReturnType; - } -} diff --git a/web/src/routes/__root.tsx b/web/src/routes/__root.tsx deleted file mode 100644 index 8d7f7ed..0000000 --- a/web/src/routes/__root.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { HeadContent, Scripts, Link, createRootRoute } from "@tanstack/react-router"; - -import appCss from "../styles.css?url"; -import { Navbar } from "#/components/navbar"; -import { Footer } from "#/components/footer"; -import { HouseIcon, BookOpenIcon } from "@phosphor-icons/react"; -import { Button } from "#/components/ui/button"; - -export const Route = createRootRoute({ - head: () => ({ - meta: [ - { charSet: "utf-8" }, - { name: "viewport", content: "width=device-width, initial-scale=1" }, - { name: "theme-color", content: "#1E6B5A" }, - { title: "gpui-query - Async State Management for GPUI" }, - ], - links: [ - { rel: "stylesheet", href: appCss }, - { rel: "icon", type: "image/x-icon", href: "/favicon.ico" }, - { rel: "manifest", href: "/manifest.json" }, - { - rel: "preconnect", - href: "https://fonts.googleapis.com", - }, - { - rel: "preconnect", - href: "https://fonts.gstatic.com", - crossOrigin: "anonymous", - }, - { - rel: "stylesheet", - href: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Oxanium:wght@400;500;600;700&display=swap", - }, - ], - scripts: [], - }), - shellComponent: RootDocument, - notFoundComponent: NotFoundPage, - errorComponent: ErrorPage, -}); - -function RootDocument({ children }: { children: React.ReactNode }) { - return ( - - -