Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
39db346
chore: move to freeoxide org and gpui-query.freeoxide.com
hmziqrs Jul 1, 2026
f062dde
Variants for images and landing pages
hmziqrs Jul 3, 2026
b417ae4
Add sections and SEO to V4 landing page
hmziqrs Jul 8, 2026
d5ea6be
Replace About page with Privacy and Terms pages
hmziqrs Jul 8, 2026
4168271
Migrate blog posts from TSX to MDX and clean up formatting
hmziqrs Jul 8, 2026
653433e
Rename website directory to docs and remove why-gpui-query blog post
hmziqrs Jul 8, 2026
c067205
Fix trailing slash redirects and SEO metadata
hmziqrs Jul 8, 2026
3d49454
Promote V4 landing page to homepage
hmziqrs Jul 8, 2026
adb6593
Improve SEO markup and fix canonical URLs
hmziqrs Jul 9, 2026
ce8c707
Upgrade to Node 24 and add per-post blog OG images
hmziqrs Jul 9, 2026
f35bebd
Consolidate OG image generation into single script
hmziqrs Jul 9, 2026
3ed8673
Extract LegalPage and LegalSection components
hmziqrs Jul 9, 2026
1faada3
Add Firebase Analytics page view tracking
hmziqrs Jul 9, 2026
6628734
Reduce entry chunk size
hmziqrs Jul 9, 2026
194b42f
Lazy load MobileNav and SearchDialog
hmziqrs Jul 9, 2026
99f47c3
Pin vite-plus dependencies to 0.1.24
hmziqrs Jul 9, 2026
d0a8b10
Add Astro and Starlight migration plan
hmziqrs Jul 10, 2026
2ce4da8
Migrate web app to Astro and Starlight
hmziqrs Jul 21, 2026
7d93a1a
Widen page layouts and grid blog posts
hmziqrs Jul 21, 2026
1ae3d42
Bump version to 0.2.0 and sync docs in CI
hmziqrs Jul 21, 2026
56014e3
Add HTTP caching and persistence documentation
hmziqrs Jul 21, 2026
72f58f8
ci: drop stale docs/ step from pr-checks
hmziqrs Jul 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions .github/workflows/changelog-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -115,28 +125,33 @@ 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:
run:
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
Expand Down
7 changes: 3 additions & 4 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ on:
branches: [master]
paths:
- 'web/**'
- 'website/**'
- 'shared/**'
- '.github/workflows/deploy.yml'
workflow_dispatch:
Expand All @@ -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
Expand Down
7 changes: 1 addition & 6 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
pull_request:
paths:
- "web/**"
- "website/**"
- "shared/**"
- ".github/workflows/pr-checks.yml"

Expand All @@ -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:
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,3 +20,7 @@ Thumbs.db

# Local Playwright/MCP artifacts
.playwright-mcp/

.wrangler/cache

.astro-migration-baseline/
63 changes: 58 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,61 @@ 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<T>` fetcher result wrapper in `core`: return `Result<Fetched<T>, 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<B>` 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.
- readme field on gpui-query-legacy so its README renders on crates.io.

## [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.
Expand All @@ -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.
Expand All @@ -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`.
Expand All @@ -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
Expand Down Expand Up @@ -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
53 changes: 31 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -85,20 +85,23 @@ fn render(&mut self, cx: &mut ViewContext<Self>) -> 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<T,E>`, `MutationResource<V,T,E>`, `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<T,E>`. 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<T,E>`. 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
Expand Down Expand Up @@ -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<DehydratedEntry> {
// Read from disk, database, etc.
}
struct MyPersister; // your backend: file, db, kv, …

fn save(&self, entries: Vec<DehydratedEntry>) {
// Write to disk, database, etc.
}
impl Persister for MyPersister {
async fn load(&self) -> Result<PersistSnapshot, PersistError> { /* … */ }
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<T,E>` through a `SelectTransform<T,U>`, giving you a `MappedQueryResource` that derives values from cached data without extra fetches.
`use_query_select` projects a `QueryResource<T,E>` through a `SelectTransform<T,U>` 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.

Expand All @@ -252,9 +261,9 @@ Garbage collection runs on idle resources older than `gc_time_ms` (default: 5 mi

## links

- Documentation: <https://gpui-query.hmziq.xyz/docs/>
- Website: <https://gpui-query.hmziq.xyz>
- Source: <https://github.com/hmziqrs/gpui-query>
- Documentation: <https://gpui-query.freeoxide.com/docs/>
- Website: <https://gpui-query.freeoxide.com>
- Source: <https://github.com/freeoxide/gpui-query>
- GPUI framework: <https://github.com/zed-industries/zed/tree/main/crates/gpui>
- TanStack Query: <https://tanstack.com/query>

Expand Down
Loading
Loading