+
+```
+
+```html title="Navigation.vue"
+
+
+
+
+
+
+
+
+
+```
+
+[](https://react.dev/learn/passing-data-deeply-with-context)
+
+Do not [prop drill](https://react.dev/learn/passing-data-deeply-with-context#the-problem-with-passing-props). Instead, [useSuspense()](./useSuspense.md) in the components that render the data from it. This is
+known as _data co-location_.
+
+Do not hide data binding hooks inside custom hooks. Instead, put tightly coupled data transformations
+in [Query](https://dataclient.io/rest/api/Query) — data logic belongs with the data model, where it stays visible, reusable,
+and free to change independently of the view.
+
+Instead of writing complex update functions or invalidations cascades, Reactive Data Client automatically updates
+bound components immediately upon [data change](./mutations.md). This is known as _reactive programming_.
+
+## Loading and Error {#async-fallbacks}
+
+You might have noticed the return type shows the value is always there. [useSuspense()](./useSuspense.md) operates very much
+with [await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await). This enables
+us to make error/loading disjoint from data usage.
+
+### Async Boundaries {#boundaries}
+
+Instead we place Vue's built-in [\](https://vuejs.org/guide/built-ins/suspense.html) along with [onErrorCaptured()](https://vuejs.org/api/composition-api-lifecycle.html#onerrorcaptured) to handling loading and error conditions at or above navigational boundaries like **pages,
+routes, or [modals](https://www.appcues.com/blog/modal-dialog-windows)**.
+
+```html title="Dashboard.vue" {13-20}
+
+
+
+
+
Dashboard
+
+
Error: {{ error.message }}
+
+
+
+
+
+
+
+
+
+
+
+```
+
+Centralizing fallbacks this way eliminates redundant loading indicators while keeping components reusable.
+The loading fallback is customized with the `#fallback` slot of [\](https://vuejs.org/guide/built-ins/suspense.html#loading-state),
+and the error fallback by rendering what you choose from [onErrorCaptured()](https://vuejs.org/api/composition-api-lifecycle.html#onerrorcaptured).
+
+### Stateful
+
+You may find cases where it's still useful to use a stateful approach to fallbacks.
+For these cases, or compatibility with some component libraries, [useDLE()](./useDLE.md) - \[D]ata \[L]oading \[E]rror - is provided.
+
+```typescript title="ProfileResource"
+import { Entity, resource } from '@data-client/rest';
+
+export class Profile extends Entity {
+ id: number | undefined = undefined;
+ avatar = '';
+ fullName = '';
+ bio = '';
+
+ static key = 'Profile';
+}
+
+export const ProfileResource = resource({
+ path: '/profiles/:id',
+ schema: Profile,
+});
+```
+
+```html title="ProfileList.vue" {5}
+
+
+
+
Error {{ error.status }}
+
+
+
+
+
+
{{ profile.fullName }}
+
{{ profile.bio }}
+
+
+
+
+```
+
+Since [useDLE](./useDLE.md) does not [useSuspense](./useSuspense.md), you won't be able to easily centrally
+orchestrate loading and error code.
+
+## Conditional
+
+> **Tip: Conditional Dependencies**
+>
+> Use `null` as the second argument to any Data Client hook means "do nothing."
+>
+> ```typescript
+> // todo could be undefined if id is undefined
+> const todo = await useSuspense(
+> TodoResource.get,
+> computed(() => (id.value ? { id: id.value } : null)),
+> );
+> ```
+
+## Subscriptions
+
+When data is likely to change due to external factor; [useSubscription()](./useSubscription.md)
+ensures continual updates while a component is mounted. [useLive()](./useLive.md) calls both
+[useSubscription()](./useSubscription.md) and [useSuspense()](./useSuspense.md), making it quite
+easy to use fresh data.
+
+```typescript title="Ticker" {32}
+import { Entity, RestEndpoint } from '@data-client/rest';
+
+export class Ticker extends Entity {
+ product_id = '';
+ trade_id = 0;
+ price = 0;
+ size = '0';
+ time = Temporal.Instant.fromEpochMilliseconds(0);
+ bid = '0';
+ ask = '0';
+ volume = '';
+
+ pk(): string {
+ return this.product_id;
+ }
+ static key = 'Ticker';
+
+ static schema = {
+ price: Number,
+ time: Temporal.Instant.from,
+ };
+}
+
+export const getTicker = new RestEndpoint({
+ urlPrefix: 'https://api.exchange.coinbase.com',
+ path: '/products/:productId/ticker',
+ schema: Ticker,
+ process(value, { productId }) {
+ value.product_id = productId;
+ return value;
+ },
+ pollFrequency: 2000,
+});
+```
+
+```html title="AssetPrice.vue" {6}
+
+
+
+
+ {{ productId }}
+
+
+
+```
+
+Subscriptions are orchestrated by [Managers](https://dataclient.io/vue/api/Manager). Out of the box,
+polling based subscriptions can be used by adding [pollFrequency](https://dataclient.io/rest/api/Endpoint#pollfrequency) to an Endpoint or Resource.
+For pushed based networking protocols like SSE and websockets, see the [example stream manager](https://dataclient.io/vue/concepts/managers#data-stream).
+
+```typescript
+export const getTicker = new RestEndpoint({
+ urlPrefix: 'https://api.exchange.coinbase.com',
+ path: '/products/:productId/ticker',
+ schema: Ticker,
+ pollFrequency: 2000,
+});
+```
diff --git a/.agents/skills/data-client-vue/references/devtools-debugging.md b/.agents/skills/data-client-vue/references/devtools-debugging.md
new file mode 100644
index 000000000000..a6551fccbb53
--- /dev/null
+++ b/.agents/skills/data-client-vue/references/devtools-debugging.md
@@ -0,0 +1,366 @@
+# Debugging Data Client with Chrome DevTools MCP
+
+Debug `@data-client/react` or `@data-client/vue` state and actions programmatically via Chrome DevTools MCP `evaluate_script`. The app's `DevToolsManager` exposes the Controller on `globalThis.__DC_CONTROLLERS__` (a `Map` keyed by `devtoolsName`) in dev mode.
+
+## Prerequisites
+
+1. Dev server running with `NODE_ENV !== 'production'`
+2. Chrome DevTools MCP connected and page loaded
+3. `DevToolsManager` included in `DataProvider` (React) or `DataClientPlugin` (Vue) managers (default in dev mode)
+
+## Step 1: Access the Controller
+
+`DevToolsManager` registers controllers in `globalThis.__DC_CONTROLLERS__` keyed by
+`devtoolsName` — defaults to `"Data Client: "`.
+
+### Discover available controllers
+
+```js
+// evaluate_script
+() => {
+ const m = globalThis.__DC_CONTROLLERS__;
+ if (!m || m.size === 0) return 'no controllers registered';
+ return [...m.keys()];
+}
+```
+
+### Get a controller by key
+
+Use the key from discovery. Always use `.get(devtoolsName)` with the actual key — not `.values().next().value` — so you target the correct store when multiple `DataProvider`s or Vue apps exist.
+
+```js
+// evaluate_script
+() => {
+ const ctrl = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App');
+ if (!ctrl) return 'not found';
+ return { ok: true, stateKeys: Object.keys(ctrl.getState()) };
+}
+```
+
+## Step 2: Install the Debug Shim
+
+Run this **once** after the page loads. It wraps dispatch to capture all actions in a circular buffer.
+
+```js
+// evaluate_script
+(() => {
+ const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value;
+ if (!ctrl) return { error: 'No controller found' };
+
+ globalThis.__DC_ACTION_LOG__ = [];
+ const MAX_LOG = 200;
+
+ const origDispatch = ctrl._dispatch.bind(ctrl);
+ ctrl._dispatch = (action) => {
+ const entry = {
+ type: action.type,
+ key: action.key,
+ ts: Date.now(),
+ };
+
+ if (action.endpoint) entry.endpoint = action.endpoint.name || action.endpoint.key;
+ if (action.args) entry.args = JSON.parse(JSON.stringify(action.args));
+ if (action.meta?.date) entry.date = action.meta.date;
+ if (action.error) entry.error = true;
+
+ globalThis.__DC_ACTION_LOG__.push(entry);
+ if (globalThis.__DC_ACTION_LOG__.length > MAX_LOG) {
+ globalThis.__DC_ACTION_LOG__ = globalThis.__DC_ACTION_LOG__.slice(-MAX_LOG / 2);
+ }
+
+ return origDispatch(action);
+ };
+
+ return { ok: true, stateKeys: Object.keys(ctrl.getState()) };
+})()
+```
+
+## Step 3: Read State
+
+### High-level queries (denormalized, schema-aware)
+
+Controller provides `getResponse`, `getError`, and `get` that denormalize through schemas — pass `ctrl.getState()` as the last argument.
+
+```js
+// evaluate_script — get denormalized response for an endpoint
+async () => {
+ const mod = await import('/src/resources/Todo.ts');
+ const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value;
+ const state = ctrl.getState();
+ const { data, expiryStatus, expiresAt } = ctrl.getResponse(
+ mod.TodoResource.getList,
+ {},
+ state,
+ );
+ return { data, expiryStatus, expiresAt };
+}
+```
+
+```js
+// evaluate_script — check if an endpoint has an error
+async () => {
+ const mod = await import('/src/resources/Todo.ts');
+ const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value;
+ const state = ctrl.getState();
+ const error = ctrl.getError(
+ mod.TodoResource.get,
+ { id: '5' },
+ state,
+ );
+ return { error: error?.message ?? null };
+}
+```
+
+```js
+// evaluate_script — query a Queryable schema (Entity, Collection, Query)
+async () => {
+ const mod = await import('/src/resources/Todo.ts');
+ const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value;
+ const state = ctrl.getState();
+ const result = ctrl.get(mod.Todo, { id: '5' }, state);
+ return result;
+}
+```
+
+### Raw normalized state inspection
+
+Use these when you need to see the raw cache structure without denormalization.
+
+```js
+// evaluate_script — state overview
+() => {
+ const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value;
+ const state = ctrl.getState();
+ return {
+ entityTypes: Object.keys(state.entities),
+ endpointCount: Object.keys(state.endpoints).length,
+ metaCount: Object.keys(state.meta).length,
+ optimisticCount: state.optimistic.length,
+ lastReset: state.lastReset,
+ };
+}
+```
+
+```js
+// evaluate_script — inspect specific entity type by key
+() => {
+ const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value;
+ const entities = ctrl.getState().entities['Todo'];
+ if (!entities) return { error: 'Entity not found' };
+ const pks = Object.keys(entities);
+ return {
+ count: pks.length,
+ samplePKs: pks.slice(0, 10),
+ sample: pks.length > 0 ? entities[pks[0]] : null,
+ };
+}
+```
+
+```js
+// evaluate_script — find endpoint cache entries by path substring
+() => {
+ const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value;
+ const state = ctrl.getState();
+ const keys = Object.keys(state.endpoints).filter(k => k.includes('/todos'));
+ return keys.map(k => ({
+ key: k,
+ value: state.endpoints[k],
+ meta: state.meta[k],
+ }));
+}
+```
+
+### Inspect a specific entity by pk
+
+```js
+// evaluate_script
+() => {
+ const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value;
+ return ctrl.getState().entities?.['Todo']?.['5'];
+}
+```
+
+### Check endpoint metadata (expiry, errors, invalidation)
+
+```js
+// evaluate_script
+() => {
+ const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value;
+ const state = ctrl.getState();
+ const key = Object.keys(state.meta).find(k => k.includes('/todos'));
+ return key ? { key, ...state.meta[key] } : 'no meta found';
+}
+```
+
+## Step 4: Track Actions
+
+After installing the debug shim (Step 2):
+
+### Read recent actions
+
+```js
+// evaluate_script
+() => globalThis.__DC_ACTION_LOG__?.slice(-20) ?? []
+```
+
+### Filter by action type
+
+```js
+// evaluate_script — track only fetches
+() => (globalThis.__DC_ACTION_LOG__ ?? [])
+ .filter(a => a.type === 'rdc/fetch' || a.type === 'rdc/setresponse')
+ .slice(-20)
+```
+
+### Filter errors
+
+```js
+// evaluate_script
+() => (globalThis.__DC_ACTION_LOG__ ?? []).filter(a => a.error)
+```
+
+### Clear action log
+
+```js
+// evaluate_script
+() => { globalThis.__DC_ACTION_LOG__ = []; return { cleared: true }; }
+```
+
+## Step 5: Mutate State via Controller
+
+Use Controller methods — **never** dispatch raw actions.
+
+### Invalidate an endpoint (force refetch)
+
+```js
+// evaluate_script — triggers refetch for subscribed components
+async () => {
+ const mod = await import('/src/resources/Todo.ts');
+ const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value;
+ await ctrl.invalidate(mod.TodoResource.get, { id: '5' });
+ return { invalidated: true };
+}
+```
+
+### Invalidate endpoints matching a pattern
+
+```js
+// evaluate_script
+() => {
+ const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value;
+ ctrl.invalidateAll({ testKey: key => key.includes('/todos') });
+ return 'todo endpoints invalidated';
+}
+```
+
+### Expire endpoints (mark stale, refetch on next use)
+
+```js
+// evaluate_script
+() => {
+ const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value;
+ ctrl.expireAll({ testKey: key => key.includes('/todos') });
+ return 'todo endpoints expired';
+}
+```
+
+### Set a value directly
+
+```js
+// evaluate_script — use setResponse to inject mock data
+async () => {
+ const mod = await import('/src/resources/Todo.ts');
+ const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value;
+ await ctrl.setResponse(
+ mod.TodoResource.getList,
+ {},
+ [{ id: 1, title: 'Mock Todo', completed: false }],
+ );
+ return { set: true };
+}
+```
+
+### Reset entire store
+
+```js
+// evaluate_script
+() => {
+ const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value;
+ ctrl.resetEntireStore();
+ return { reset: true };
+}
+```
+
+## Correlating with Network Requests
+
+Use `list_network_requests` with `resourceTypes: ["fetch", "xhr"]` to see API calls,
+then cross-reference with endpoint keys in state.
+
+## Action Types Reference
+
+| Type | Controller Method | Description |
+|---|---|---|
+| `rdc/fetch` | `fetch()` | Network request initiated |
+| `rdc/setresponse` | `setResponse()` | Response written to cache |
+| `rdc/set` | `set()` | Direct entity value set |
+| `rdc/optimistic` | (automatic) | Optimistic update applied |
+| `rdc/invalidate` | `invalidate()` | Single endpoint invalidated |
+| `rdc/invalidateall` | `invalidateAll()` | Bulk invalidation by key test |
+| `rdc/expireall` | `expireAll()` | Bulk mark-stale by key test |
+| `rdc/reset` | `resetEntireStore()` | Full store reset |
+| `rdc/subscribe` | `subscribe()` | Subscription registered |
+| `rdc/unsubscribe` | `unsubscribe()` | Subscription removed |
+| `rdc/gc` | (automatic) | Garbage collection |
+
+## Controller State Readers
+
+All take `state` (from `ctrl.getState()`) as the **last** argument.
+
+| Method | Signature | Returns |
+|---|---|---|
+| `getResponse` | `(endpoint, ...args, state)` | `{ data, expiryStatus, expiresAt }` — denormalized through schema |
+| `getError` | `(endpoint, ...args, state)` | `ErrorTypes \| undefined` |
+| `get` | `(schema, ...args, state)` | `Denormalized \| undefined` for any Queryable schema |
+| `getQueryMeta` | `(schema, ...args, state)` | `{ data, countRef }` |
+
+`expiryStatus` values: `1` = Invalid, `2` = InvalidIfStale, `3` = Valid.
+
+## State Shape Reference
+
+```ts
+State = {
+ entities: { [entityKey: string]: { [pk: string]: EntityInstance } },
+ endpoints: { [cacheKey: string]: PK | PK[] | unknown },
+ indexes: { [entityKey: string]: { [indexName: string]: { [lookupValue: string]: PK } } },
+ meta: {
+ [key: string]: {
+ date, fetchedAt, expiresAt,
+ prevExpiresAt?, error?, invalidated?, errorPolicy?: 'hard' | 'soft'
+ }
+ },
+ entitiesMeta: { [entityKey: string]: { [pk: string]: { date, expiresAt, fetchedAt } } },
+ optimistic: (SetResponseAction | OptimisticAction)[],
+ lastReset: number,
+}
+```
+
+## Polling Pattern
+
+For monitoring ongoing activity, poll with short intervals:
+
+1. Install shim (Step 2)
+2. Trigger the user action or navigation
+3. Wait 2–3 seconds
+4. Read actions (Step 4) — check for `rdc/fetch` then `rdc/setresponse` pairs
+5. If needed, read entity state (Step 3) to verify cache contents
+6. Repeat if watching for subscription updates
+
+## Debugging Checklist
+
+1. **Verify controller exists**: Check `__DC_CONTROLLERS__` map size
+2. **Inspect state shape**: Get entity types and endpoint count
+3. **Check specific data**: Look up entities by type and pk
+4. **Review endpoint metadata**: Check expiry, errors, invalidation status
+5. **Track actions**: Read the action log for recent dispatches
+6. **Correlate network**: Compare `list_network_requests` with endpoint keys
+7. **Force refresh**: Use `invalidateAll` or `expireAll` to trigger refetches
diff --git a/.agents/skills/data-client-vue/references/getDefaultManagers.md b/.agents/skills/data-client-vue/references/getDefaultManagers.md
new file mode 100644
index 000000000000..79cc0d6e9040
--- /dev/null
+++ b/.agents/skills/data-client-vue/references/getDefaultManagers.md
@@ -0,0 +1,112 @@
+
+
+# getDefaultManagers()
+
+`getDefaultManagers` returns an Array of [Managers](https://dataclient.io/vue/api/Manager) to be sent to [DataClientPlugin](./installation.md#add-provider-at-top-level-component).
+
+This makes it simple to configure and add custom [Managers](https://dataclient.io/vue/api/Manager), while remaining robust against
+any potential changes to the default managers.
+
+Currently returns \[[DevToolsManager](https://dataclient.io/vue/api/DevToolsManager)\*, [NetworkManager](https://dataclient.io/vue/api/NetworkManager), [SubscriptionManager](https://dataclient.io/vue/api/SubscriptionManager)].
+
+\*(`DevToolsManager` is excluded in production builds.)
+
+## Usage
+
+```ts title="main.ts"
+import { createApp } from 'vue';
+import { DataClientPlugin, getDefaultManagers } from '@data-client/vue';
+import App from './App.vue';
+
+const managers = getDefaultManagers({
+ // set fallback expiry time to an hour
+ networkManager: { dataExpiryLength: 1000 * 60 * 60 },
+});
+
+const app = createApp(App);
+app.use(DataClientPlugin, { managers });
+app.mount('#app');
+```
+
+When `managers` is omitted, `DataClientPlugin` uses `getDefaultManagers()` with no arguments.
+See [installation](./installation.md#add-provider-at-top-level-component) for the
+other `DataClientPlugin` options.
+
+## Arguments
+
+Each argument represents a configuration of the manager. It can be of three possible types:
+
+- Any plain object is used as options to be sent to the manager's constructor.
+- An instance of the manager to be used directly.
+- `null`. When sent will exclude the manager.
+
+```ts
+getDefaultManagers({
+ devToolsManager: { trace: true },
+ networkManager: new NetworkManager({ errorExpiryLength: 1 }),
+ subscriptionManager: null,
+});
+```
+
+### networkManager
+
+> **Note**
+>
+> `null` is not allowed here since NetworkManager is required
+
+`dataExpiryLength` is used as a fallback when an Endpoint does not have [dataExpiryLength](https://dataclient.io/docs/concepts/expiry-policy#endpointdataexpirylength) defined.
+
+`errorExpiryLength` is used as a fallback when an Endpoint does not have [errorExpiryLength](https://dataclient.io/docs/concepts/expiry-policy#endpointerrorexpirylength) defined.
+
+### devToolsManager
+
+[Arguments](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Arguments.md)
+to send to redux devtools.
+
+### subscriptionManager
+
+A class that implements `SubscriptionConstructable` like [PollingSubscription](https://dataclient.io/vue/api/PollingSubscription)
+
+## Examples
+
+### Tracing actions
+
+For example, we can enable the [trace](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Arguments.md#trace) option to help track down where actions are dispatched from. This has a large performance impact, so it is normally disabled.
+
+```ts
+const managers = getDefaultManagers({
+ devToolsManager: { trace: true },
+});
+```
+
+### Manager inheritance
+
+Sending manager instances allows us to customize managers using inheritance.
+
+```ts
+import { NetworkManager, type FetchAction } from '@data-client/vue';
+
+class LoggingNetworkManager extends NetworkManager {
+ protected handleFetch(action: FetchAction) {
+ console.log('fetching', action.key);
+ return super.handleFetch(action);
+ }
+}
+
+const managers = getDefaultManagers({
+ networkManager: new LoggingNetworkManager(),
+});
+```
+
+### Disabling
+
+Using `null` will remove managers completely. [NetworkManager](https://dataclient.io/vue/api/NetworkManager) cannot be removed this way.
+
+```ts
+const managers = getDefaultManagers({
+ devToolsManager: null,
+ subscriptionManager: null,
+});
+```
+
+Here we disable every manager except [NetworkManager](https://dataclient.io/vue/api/NetworkManager).
diff --git a/.agents/skills/data-client-vue/references/installation.md b/.agents/skills/data-client-vue/references/installation.md
new file mode 100644
index 000000000000..bc39ca65a263
--- /dev/null
+++ b/.agents/skills/data-client-vue/references/installation.md
@@ -0,0 +1,79 @@
+
+
+# Getting Started with Reactive Data Client
+
+> **Tip: Use Agent Skills**
+>
+> Prefer to scaffold via your AI agent? See [Agent Skills](https://dataclient.io/vue/getting-started/agent-skills) and run `/data-client-setup`.
+
+## Install the plugin {#add-provider-at-top-level-component}
+
+Install the [Vue plugin](https://vuejs.org/guide/reusability/plugins.html) when creating your app.
+
+```bash
+npm install @data-client/vue @data-client/rest
+```
+
+```tsx title="main.ts"
+import { createApp } from 'vue';
+import { DataClientPlugin } from '@data-client/vue';
+
+const app = createApp(App);
+
+app.use(DataClientPlugin, {
+ // optional overrides
+ // managers: getDefaultManagers(),
+ // initialState,
+ // Controller,
+ // gcPolicy,
+});
+
+app.mount('#app');
+```
+
+[Next: Define Data »](https://dataclient.io/vue/getting-started/resource)
+
+## Example
+
+Example app: [vue-todo-app](https://github.com/reactive/data-client/tree/master/examples/vue-todo-app) ([`src/main.ts`](https://github.com/reactive/data-client/blob/master/examples/vue-todo-app/src/main.ts), [`src/pages/UserTodos.vue`](https://github.com/reactive/data-client/blob/master/examples/vue-todo-app/src/pages/UserTodos.vue))
+
+## Supported Tools
+
+
+
+TypeScript 4.0+
+
+TypeScript is optional, but requires at least version [4.0](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#variadic-tuple-types) and [strictNullChecks](https://www.typescriptlang.org/tsconfig#strictNullChecks) for full type enforcement.
+
+
+
+
+
+Older browser support
+
+If your application targets older browsers (a few years or more), be sure to load polyfills.
+Typically this is done with [@babel/preset-env useBuiltIns: 'entry'](https://babeljs.io/docs/en/babel-preset-env#usebuiltins),
+coupled with importing [core-js](https://www.npmjs.com/package/core-js) at the entrypoint of your application.
+
+This ensures only the needed polyfills for your browser support targets are included in your application bundle.
+
+For instance `TypeError: Object.hasOwn is not a function`
+
+
+
+
+
+Internet Explorer support
+
+If you see `Uncaught TypeError: Class constructor Resource cannot be invoked without 'new'`,
+follow the instructions to [add legacy browser support to packages](https://dataclient.io/vue/guides/legacy-browser)
+
+
+
+
+
+Vue 3
+
+`@data-client/vue` supports Vue 3 and is built on the [Composition API](https://vuejs.org/guide/extras/composition-api-faq.html).
+
+
diff --git a/.agents/skills/data-client-vue/references/mutations.md b/.agents/skills/data-client-vue/references/mutations.md
new file mode 100644
index 000000000000..edc5f7b3b1b5
--- /dev/null
+++ b/.agents/skills/data-client-vue/references/mutations.md
@@ -0,0 +1,376 @@
+
+
+# Data mutations
+
+Using our [Create, Update, and Delete](https://dataclient.io/vue/concepts/atomic-mutations) endpoints with
+[Controller.fetch()](./Controller.md#fetch) reactively updates _all_ appropriate components atomically (at the same time).
+
+[useController()](./useController.md) gives components access to this global supercharged [setState()](https://react.dev/reference/react/useState#setstate).
+
+[//]: # "TODO: Add create, and delete examples as well (in tabs)"
+
+```ts title="TodoResource"
+import { Entity, resource } from '@data-client/rest';
+
+export class Todo extends Entity {
+ id = 0;
+ userId = 0;
+ title = '';
+ completed = false;
+
+ static key = 'Todo';
+}
+export const TodoResource = resource({
+ urlPrefix: 'https://jsonplaceholder.typicode.com',
+ path: '/todos/:id',
+ searchParams: {} as { userId?: string | number } | undefined,
+ schema: Todo,
+ optimistic: true,
+});
+```
+
+```html title="TodoItem.vue" {8-12,14-16}
+
+
+
+
+
+```
+
+[getOptimisticResponse](https://dataclient.io/rest/guides/optimistic-updates) is just like [setState with an updater function](https://react.dev/reference/react/useState#updating-state-based-on-the-previous-state). [Snapshot](https://dataclient.io/vue/api/Snapshot) provides typesafe access to the previous store value,
+which we use to return the _expected_ fetch response.
+
+Reactive Data Client ensures [data integrity against any possible networking failure or race condition](https://dataclient.io/rest/guides/optimistic-updates#optimistic-transforms), so don't
+worry about network failures, multiple mutation calls editing the same data, or other common
+problems in asynchronous programming.
+
+## Tracking mutation loading
+
+[useLoading()](./useLoading.md) enhances async functions by tracking their loading and error states.
+
+```ts title="PostResource"
+import { Entity, resource } from '@data-client/rest';
+
+export class Post extends Entity {
+ id = 0;
+ author = 0;
+ title = '';
+ body = '';
+ votes = 0;
+
+ static key = 'Post';
+
+ get img() {
+ return `//loremflickr.com/96/72/kitten,cat?lock=${this.id % 16}`;
+ }
+}
+export const PostResource = resource({
+ path: '/posts/:id',
+ schema: Post,
+});
+```
+
+```html title="PostDetail.vue"
+
+
+
+
+
+
+```
diff --git a/.agents/skills/data-client-vue/references/useCache.md b/.agents/skills/data-client-vue/references/useCache.md
new file mode 100644
index 000000000000..28a29d8000a4
--- /dev/null
+++ b/.agents/skills/data-client-vue/references/useCache.md
@@ -0,0 +1,130 @@
+
+
+# useCache()
+
+Data rendering without the fetch.
+
+Access any [Endpoint](https://dataclient.io/rest/api/Endpoint)'s response. If the response does not exist, returns
+`undefined`. This can be used to check for an `Endpoint's` existance like for authentication.
+
+`useCache()` is reactive to data [mutations](./mutations.md); rerendering only when necessary.
+
+## Usage
+
+```ts title="UserResource"
+import { Entity, resource } from '@data-client/rest';
+
+export class User extends Entity {
+ id = '';
+ name = '';
+ isAdmin = false;
+
+ static key = 'User';
+}
+export const UserResource = resource({
+ path: '/users/:id',
+ schema: User,
+}).extend('current', {
+ path: '/user',
+ schema: User,
+});
+```
+
+```html title="Unauthed.vue"
+
+
+
+
+
Not authorized
+ logging in...
+
+
+
+```
+
+```html title="Authorized.vue"
+
+
+
+
+
Welcome, {{ user.name }}!
+
+
+
+```
+
+```html title="AuthorizedPage.vue"
+
+
+
+
+
+
+
+
+```
+
+See [truthiness narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#truthiness-narrowing) for
+more information about type handling
+
+## Behavior
+
+`useCache()` returns a [ComputedRef](https://vuejs.org/api/reactivity-core.html#computed). The table
+below describes its `.value`.
+
+| Expiry Status | Returns | Conditions |
+| ------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Invalid | `undefined` | not in store, [deletion](https://dataclient.io/rest/api/resource#delete), [invalidation](./Controller.md#invalidate), [invalidIfStale](https://dataclient.io/vue/concepts/expiry-policy#endpointinvalidifstale) |
+| Stale | denormalized | (first-render, arg change) & [expiry < now](https://dataclient.io/vue/concepts/expiry-policy) |
+| Valid | denormalized | fetch completion |
+| | `undefined` | `null` used as second argument |
+
+> **Tip: Conditional Dependencies**
+>
+> Use `null` as the second argument to any Data Client hook means "do nothing."
+>
+> ```typescript
+> // todo could be undefined if id is undefined
+> const todo = useCache(
+> TodoResource.get,
+> computed(() => (id.value ? { id: id.value } : null)),
+> );
+> ```
+
+## Types
+
+```typescript
+function useCache(
+ endpoint: ReadEndpoint,
+ ...args: MaybeRefsOrGetters> | [null]
+): ComputedRef>;
+```
+
+Arguments can be plain values, [refs](https://vuejs.org/api/reactivity-core.html#ref) (including [computed](https://vuejs.org/api/reactivity-core.html#computed)), or getter
+functions like `() => ({ id: props.id })`. A plain object like `{ id: props.id }` is read once and won't
+follow prop or route changes, so use a getter or `computed` when an argument can change.
+
+The result updates when the arguments change.
diff --git a/.agents/skills/data-client-vue/references/useController.md b/.agents/skills/data-client-vue/references/useController.md
new file mode 100644
index 000000000000..6ad0b73f00d9
--- /dev/null
+++ b/.agents/skills/data-client-vue/references/useController.md
@@ -0,0 +1,170 @@
+
+
+# useController()
+
+[Controller](./Controller.md) provides type-safe methods to access and dispatch actions to the store.
+
+For instance [fetch](./Controller.md#fetch), [invalidate](./Controller.md#invalidate),
+and [setResponse](./Controller.md#setResponse)
+
+```html
+
+```
+
+`useController()` must be called inside `
+
+
+
+
+```
+
+### Direct entity update
+
+Use [set](./Controller.md#set) for immediate updates without network requests. Supports functional updates to avoid race conditions.
+
+```html title="VoteButton.vue"
+
+
+
+
+
+```
+
+### Invalidate after mutation
+
+Force refetch of related data using [invalidate](./Controller.md#invalidate) or [expireAll](./Controller.md#expireAll).
+
+```html title="ClearUserCache.vue"
+
+
+
+
+
+```
+
+> **Tip**
+>
+> For better performance and consistency, prefer [including side effect updates in mutation responses](https://dataclient.io/rest/guides/side-effects).
+
+### Prefetching
+
+Use [fetchIfStale](./Controller.md#fetchIfStale) to prefetch without overfetching fresh data.
+
+```html title="ArticleLink.vue"
+
+
+
+
+ Read more
+
+
+```
+
+### Websocket updates
+
+Populate cache with external data via [set](./Controller.md#set).
+
+```ts title="useWebsocket.ts"
+import { onMounted, onUnmounted } from 'vue';
+import { useController } from '@data-client/vue';
+
+export function useWebsocket(url: string) {
+ const ctrl = useController();
+ let ws: WebSocket;
+
+ onMounted(() => {
+ ws = new WebSocket(url);
+ ws.onmessage = event => {
+ const { entity, args, data } = JSON.parse(event.data);
+ ctrl.set(EntityMap[entity], args, data);
+ };
+ });
+ onUnmounted(() => ws?.close());
+}
+```
+
+> **Warning**
+>
+> For production use, implement a [Manager for data streams](https://dataclient.io/vue/concepts/managers#data-stream) rather than component-level lifecycle hooks. Managers handle connection lifecycle globally and work with SSR.
+
+### Todo App
+
+Example app: [vue-todo-app](https://github.com/reactive/data-client/tree/master/examples/vue-todo-app) ([`src/resources/TodoResource.ts`](https://github.com/reactive/data-client/blob/master/examples/vue-todo-app/src/resources/TodoResource.ts), [`src/components/TodoItem.vue`](https://github.com/reactive/data-client/blob/master/examples/vue-todo-app/src/components/TodoItem.vue))
diff --git a/.agents/skills/data-client-vue/references/useDLE.md b/.agents/skills/data-client-vue/references/useDLE.md
new file mode 100644
index 000000000000..ad20c87ccdf8
--- /dev/null
+++ b/.agents/skills/data-client-vue/references/useDLE.md
@@ -0,0 +1,270 @@
+
+
+# useDLE() - \[D]ata \[L]oading \[E]rror
+
+High performance async data rendering without overfetching. With fetch meta data.
+
+In case you cannot use [suspense](./data-dependency.md#async-fallbacks), useDLE() is just like [useSuspense()](./useSuspense.md) but returns \[D]ata \[L]oading \[E]rror values.
+
+`useDLE()` is reactive to data [mutations](./mutations.md); rerendering only when necessary.
+
+## Usage
+
+```typescript title="ProfileResource"
+import { Entity, resource } from '@data-client/rest';
+
+export class Profile extends Entity {
+ id: number | undefined = undefined;
+ avatar = '';
+ fullName = '';
+ bio = '';
+
+ static key = 'Profile';
+}
+
+export const ProfileResource = resource({
+ path: '/profiles/:id',
+ schema: Profile,
+});
+```
+
+```html title="ProfileList.vue"
+
+
+
+
Error {{ error.status }}
+
+
+
+
+
+
{{ profile.fullName }}
+
{{ profile.bio }}
+
+
+
+
+```
+
+## Behavior
+
+`data`, `loading` and `error` are each a [ComputedRef](https://vuejs.org/api/reactivity-core.html#computed).
+Destructure them at the top level of `
+
+
+
Error {{ error.status }}
+
+
+
+
+
{{ profile.fullName }}
+
{{ profile.bio }}
+
+
+
+```
+
+### Conditional
+
+`null` will avoid binding and fetching data
+
+```ts title="Resources"
+import { Entity, resource } from '@data-client/rest';
+
+export class Post extends Entity {
+ id = 0;
+ userId = 0;
+ title = '';
+ body = '';
+
+ static key = 'Post';
+}
+export const PostResource = resource({
+ path: '/posts/:id',
+ schema: Post,
+});
+
+export class User extends Entity {
+ id = 0;
+ name = '';
+ username = '';
+ email = '';
+ phone = '';
+ website = '';
+
+ get profileImage() {
+ return `https://i.pravatar.cc/64?img=${this.id + 4}`;
+ }
+
+ static key = 'User';
+}
+export const UserResource = resource({
+ urlPrefix: 'https://jsonplaceholder.typicode.com',
+ path: '/users/:id',
+ schema: User,
+});
+```
+
+```html title="PostWithAuthor.vue" {15-21}
+
+
+
+
Error {{ postError.status }}
+
+
Error {{ authorError.status }}
+
+
{{ author.username }}
+
+```
+
+### Embedded data
+
+When entities are stored in [nested structures](https://dataclient.io/rest/guides/relational-data#nesting), that structure will remain.
+
+```typescript title="api/Post"
+export class PaginatedPost extends Entity {
+ id = '';
+ title = '';
+ content = '';
+
+ static key = 'PaginatedPost';
+}
+
+export const getPosts = new RestEndpoint({
+ path: '/post',
+ searchParams: { page: '' },
+ schema: {
+ results: new Collection([PaginatedPost]),
+ nextPage: '',
+ lastPage: '',
+ },
+});
+```
+
+```html title="ArticleList.vue" {14}
+
+
+
+
Error {{ error.status }}
+
+
+
+ {{ post.title }}
+
+
+
+```
diff --git a/.agents/skills/data-client-vue/references/useDebounce.md b/.agents/skills/data-client-vue/references/useDebounce.md
new file mode 100644
index 000000000000..bcfb7660941c
--- /dev/null
+++ b/.agents/skills/data-client-vue/references/useDebounce.md
@@ -0,0 +1,125 @@
+
+
+# useDebounce()
+
+Delays updating the parameters by [debouncing](https://css-tricks.com/debouncing-throttling-explained-examples/).
+
+Useful to avoid spamming network requests when parameters might change quickly (like a typeahead field).
+
+> **Tip**
+>
+> `useDebounce()` returns [refs](https://vuejs.org/api/reactivity-core.html#ref), so the debounced
+> value can be passed directly to other composables or components. `isPending` is true from the moment
+> the input changes until the debounced value is updated.
+
+## Usage
+
+```ts title="IssueQuery"
+import { RestEndpoint, Entity, Collection } from '@data-client/rest';
+
+export class Issue extends Entity {
+ number = 0;
+ repository_url = '';
+ labels_url = '';
+ html_url = '';
+ body = '';
+ title = '';
+ state: 'open' | 'closed' = 'open';
+ locked = false;
+ comments = 0;
+ created_at = Temporal.Instant.fromEpochMilliseconds(0);
+ updated_at = Temporal.Instant.fromEpochMilliseconds(0);
+ closed_at: Temporal.Instant | null = null;
+ authorAssociation = 'NONE';
+ pullRequest: Record | null = null;
+ declare draft?: boolean;
+
+ static schema = {
+ created_at: Temporal.Instant.from,
+ updated_at: Temporal.Instant.from,
+ closed_at: Temporal.Instant.from,
+ };
+
+ pk() {
+ return [this.repository_url, this.number].join(',');
+ }
+}
+
+export const issueQuery = new RestEndpoint({
+ urlPrefix: 'https://api.github.com',
+ path: '/search/issues',
+ searchParams: {} as { q: string },
+ paginationField: 'page',
+ schema: {
+ incomplete_results: false,
+ items: new Collection([Issue]),
+ total_count: 0,
+ },
+});
+```
+
+```html title="IssueList.vue"
+
+
+
+ {{ response.total_count }} results
+
+
+```
+
+```html title="SearchIssues.vue"
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## Types
+
+```typescript
+function useDebounce(
+ value: T | Ref,
+ delay: number,
+ updatable?: boolean | Ref,
+): [Ref, Ref];
+```
+
+`value` and `updatable` can be plain values or [refs](https://vuejs.org/api/reactivity-core.html#ref).
+Returns a tuple of `[debouncedValue, isPending]` refs. When `updatable` is `false`, the debounced
+value stops updating and `isPending` resets to `false`.
diff --git a/.agents/skills/data-client-vue/references/useFetch.md b/.agents/skills/data-client-vue/references/useFetch.md
new file mode 100644
index 000000000000..b19d1ba8e090
--- /dev/null
+++ b/.agents/skills/data-client-vue/references/useFetch.md
@@ -0,0 +1,165 @@
+
+
+# useFetch()
+
+Fetch an Endpoint if it is not in cache or stale. Returns a [Ref](https://vuejs.org/api/reactivity-core.html#ref)
+holding the fetch promise (with a `resolved` flag). A new fetch is triggered when the arguments change or
+the data is [invalidated](./Controller.md#invalidate). Use it to start fetches early, then read the data
+with [useSuspense()](./useSuspense.md), [useCache()](./useCache.md) or [useDLE()](./useDLE.md).
+
+## Usage
+
+### Parallel data loading
+
+`await useSuspense()` runs sequentially in `
+
+
+
+
{{ post.title }}
+
{{ post.body }}
+
Comments
+
+ {{ comment.author }}: {{ comment.text }}
+
+
+
+```
+
+### Prefetching
+
+`useFetch()` can also be used standalone to ensure resources are available early in a render tree before they are needed.
+
+> **Tip**
+>
+> Use in combination with a data-binding hook ([useCache()](./useCache.md), [useSuspense()](./useSuspense.md), [useDLE()](./useDLE.md), [useLive()](./useLive.md))
+> in another component.
+
+```html title="MasterPost.vue"
+
+```
+
+## Behavior
+
+| Expiry Status | Fetch | `.value` | `resolved` | Conditions |
+| ------------- | --------------- | ---------------- | ---------- | -------------------------------------------------------------------------------------------------------------------- |
+| Invalid | yes1 | pending promise | `false` | not in store, [deletion](https://dataclient.io/rest/api/resource#delete), [invalidation](./Controller.md#invalidate) |
+| Stale | yes1 | pending promise | `false` | (first-render, arg change) & [expiry < now](https://dataclient.io/vue/concepts/expiry-policy) |
+| Valid | no | resolved promise | `true` | fetch completion |
+| Error | no | rejected promise | `true` | fetch failed |
+| | no | `undefined` | | `null` used as second argument |
+
+The returned `Ref` is updated with a new promise whenever a fetch is triggered: on argument change,
+[invalidation](./Controller.md#invalidate), or [reset](./Controller.md#resetEntireStore).
+
+> **Note**
+>
+> 1. Identical fetches are automatically deduplicated
+
+> **Tip: Conditional Dependencies**
+>
+> Use `null` as the second argument to any Data Client hook means "do nothing."
+>
+> ```typescript
+> // todo could be undefined if id is undefined
+> const todo = useFetch(
+> TodoResource.get,
+> computed(() => (id.value ? { id: id.value } : null)),
+> );
+> ```
+
+## Types
+
+```typescript
+function useFetch(
+ endpoint: ReadEndpoint,
+ ...args: MaybeRefsOrGetters> | [null]
+): Readonly<
+ Ref<
+ | (Promise> & {
+ resolved: boolean;
+ })
+ | undefined
+ >
+>;
+```
+
+Arguments can be plain values, [refs](https://vuejs.org/api/reactivity-core.html#ref) (including [computed](https://vuejs.org/api/reactivity-core.html#computed)), or getter
+functions like `() => ({ id: props.id })`. A plain object like `{ id: props.id }` is read once and won't
+follow prop or route changes, so use a getter or `computed` when an argument can change.
+
+A new fetch is triggered when the arguments change.
+
+## Examples
+
+### Checking fetch status
+
+Use `promise.resolved` to check whether data is still loading:
+
+```html title="MasterPost.vue"
+
+```
diff --git a/.agents/skills/data-client-vue/references/useLive.md b/.agents/skills/data-client-vue/references/useLive.md
new file mode 100644
index 000000000000..1ea1a4682de2
--- /dev/null
+++ b/.agents/skills/data-client-vue/references/useLive.md
@@ -0,0 +1,113 @@
+
+
+# useLive()
+
+Async rendering of remotely triggered data mutations.
+
+[useSuspense()](./useSuspense.md) + [useSubscription()](./useSubscription.md) in one composable.
+
+`useLive()` is reactive to data [mutations](./mutations.md); rerendering only when necessary.
+
+## Usage
+
+```typescript title="Ticker" {32}
+import { Entity, RestEndpoint } from '@data-client/rest';
+
+export class Ticker extends Entity {
+ product_id = '';
+ trade_id = 0;
+ price = 0;
+ size = '0';
+ time = Temporal.Instant.fromEpochMilliseconds(0);
+ bid = '0';
+ ask = '0';
+ volume = '';
+
+ pk(): string {
+ return this.product_id;
+ }
+ static key = 'Ticker';
+
+ static schema = {
+ price: Number,
+ time: Temporal.Instant.from,
+ };
+}
+
+export const getTicker = new RestEndpoint({
+ urlPrefix: 'https://api.exchange.coinbase.com',
+ path: '/products/:productId/ticker',
+ schema: Ticker,
+ process(value, { productId }) {
+ value.product_id = productId;
+ return value;
+ },
+ pollFrequency: 2000,
+});
+```
+
+```html title="AssetPrice.vue"
+
+
+
+
+ {{ productId }}
+
+
+
+```
+
+Like [useSuspense()](./useSuspense.md), `useLive()` returns a Promise, so it is used with `await` in
+`
+
+
+
+
+
+```
+
+Returns the wrapped function along with `loading` and `error` [refs](https://vuejs.org/api/reactivity-core.html#ref).
+The wrapped function is stable, so no dependency list is needed: any refs or props it reads are
+read at call time.
+
+## Types
+
+```typescript
+export default function useLoading<
+ F extends (...args: any) => Promise,
+>(func: F): [F, Ref, Ref];
+```
+
+`loading` is `true` while the returned promise is pending. If `func` rejects, the rejection is
+caught and stored in `error`; it is cleared again on the next call.
+
+## Examples
+
+### Todo creation
+
+Example app: [vue-todo-app](https://github.com/reactive/data-client/tree/master/examples/vue-todo-app) ([`src/components/TodoList.vue`](https://github.com/reactive/data-client/blob/master/examples/vue-todo-app/src/components/TodoList.vue), [`src/resources/TodoResource.ts`](https://github.com/reactive/data-client/blob/master/examples/vue-todo-app/src/resources/TodoResource.ts))
diff --git a/.agents/skills/data-client-vue/references/useQuery.md b/.agents/skills/data-client-vue/references/useQuery.md
new file mode 100644
index 000000000000..f75097c8cd6e
--- /dev/null
+++ b/.agents/skills/data-client-vue/references/useQuery.md
@@ -0,0 +1,303 @@
+
+
+# useQuery()
+
+Data rendering without the fetch.
+
+Access any [Queryable Schema](https://dataclient.io/rest/api/schema#queryable)'s store value; like [Entity](https://dataclient.io/rest/api/Entity), [All](https://dataclient.io/rest/api/All), [Collection](https://dataclient.io/rest/api/Collection), [Query](https://dataclient.io/rest/api/Query),
+[Union](https://dataclient.io/rest/api/Union), and [Scalar](https://dataclient.io/rest/api/Scalar). [Lazy](https://dataclient.io/rest/api/Lazy) fields also work via their [`.query`](https://dataclient.io/rest/api/Lazy#query) accessor.
+If the value does not exist, returns `undefined`.
+
+`useQuery()` is reactive to data [mutations](./mutations.md); rerendering only when necessary. Returns `undefined`
+when data is [Invalid](https://dataclient.io/vue/concepts/expiry-policy#invalid).
+
+> **Tip**
+>
+> [Queries](https://dataclient.io/rest/api/Query) are a great companion to efficiently render aggregate computations like those that use [groupBy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/groupBy#browser_compatibility),
+> [map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map), [reduce](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce), and [filter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter).
+
+## Usage
+
+```ts title="Post"
+import { Entity, schema } from '@data-client/rest';
+
+export class Post extends Entity {
+ id = 0;
+ author = { id: 0 };
+ title = '';
+ body = '';
+ votes = 0;
+
+ static key = 'Post';
+
+ static schema = {
+ author: EntityMixin(
+ class User {
+ id = 0;
+ },
+ ),
+ };
+
+ get img() {
+ return `//loremflickr.com/96/72/kitten,cat?lock=${this.id % 16}`;
+ }
+}
+```
+
+```ts title="PostResource" {15-22}
+import { resource } from '@data-client/rest';
+import { Post } from './Post';
+
+export { Post };
+
+export const PostResource = resource({
+ path: '/posts/:id',
+ searchParams: {} as { userId?: string | number } | undefined,
+ schema: Post,
+}).extend('vote', {
+ path: '/posts/:id/vote',
+ method: 'POST',
+ body: undefined,
+ schema: Post,
+ getOptimisticResponse(snapshot, { id }) {
+ const post = snapshot.get(Post, { id });
+ if (!post) throw snapshot.abort;
+ return {
+ id,
+ votes: post.votes + 1,
+ };
+ },
+});
+```
+
+```html title="PostItem.vue" {9}
+
+
+
+
+
+```
+
+See [truthiness narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#truthiness-narrowing) for
+more information about type handling
+
+## Types
+
+```typescript
+function useQuery(
+ schema: S,
+ ...args: MaybeRefsOrGetters>
+): ComputedRef | undefined>;
+```
+
+Arguments can be plain values, [refs](https://vuejs.org/api/reactivity-core.html#ref) (including [computed](https://vuejs.org/api/reactivity-core.html#computed)), or getter
+functions like `() => ({ id: props.id })`. A plain object like `{ id: props.id }` is read once and won't
+follow prop or route changes, so use a getter or `computed` when an argument can change.
+
+The result updates when the arguments change.
+
+### Queryable
+
+[Queryable](https://dataclient.io/rest/api/schema#queryable) schemas require an `queryKey()` method that returns something. These include
+[Entity](https://dataclient.io/rest/api/Entity), [All](https://dataclient.io/rest/api/All), [Collection](https://dataclient.io/rest/api/Collection), [Query](https://dataclient.io/rest/api/Query),
+[Union](https://dataclient.io/rest/api/Union), and [Scalar](https://dataclient.io/rest/api/Scalar). [Lazy](https://dataclient.io/rest/api/Lazy) fields produce a Queryable via their [`.query`](https://dataclient.io/rest/api/Lazy#query) accessor.
+
+```ts
+interface Queryable {
+ queryKey(
+ args: readonly any[],
+ queryKey: (...args: any) => any,
+ getEntity: GetEntity,
+ getIndex: GetIndex,
+ // Must be non-void
+ ): {};
+}
+```
+
+## Examples
+
+### Sorting & Filtering
+
+[Query](https://dataclient.io/rest/api/Query) provides programmatic access to the Reactive Data Client store.
+
+```ts title="UserResource"
+export class User extends Entity {
+ id = '';
+ name = '';
+ isAdmin = false;
+
+ static key = 'User';
+}
+export const UserResource = resource({
+ path: '/users/:id',
+ schema: User,
+});
+```
+
+```html title="UsersPage.vue" {22}
+
+
+
+
No users in cache yet
+
+
{{ user.name }}
+
+
+```
+
+### Lazy relationships
+
+[Lazy](https://dataclient.io/rest/api/Lazy) fields keep raw IDs during parent denormalization. Use [`.query`](https://dataclient.io/rest/api/Lazy#query) with `useQuery` to resolve them on demand,
+isolating re-renders to only the components that need the related data.
+
+```ts title="Resources"
+export class Building extends Entity {
+ id = '';
+ name = '';
+
+ static key = 'Building';
+}
+
+export class Department extends Entity {
+ id = '';
+ name = '';
+ buildings: string[] = [];
+
+ static schema = {
+ buildings: new Lazy([Building]),
+ };
+ static key = 'Department';
+}
+
+export const DepartmentResource = resource({
+ path: '/departments/:id',
+ schema: Department,
+});
+```
+
+```html title="BuildingList.vue" {8-11}
+
+
+
+ {{ buildings.map(b => b.name).join(', ') }}
+
+```
+
+```html title="DepartmentsPage.vue"
+
+
+
+
Loading...
+
+
+ {{ dept.name }}:
+
+
+
+```
diff --git a/.agents/skills/data-client-vue/references/useSubscription.md b/.agents/skills/data-client-vue/references/useSubscription.md
new file mode 100644
index 000000000000..9ddb3aad9f0f
--- /dev/null
+++ b/.agents/skills/data-client-vue/references/useSubscription.md
@@ -0,0 +1,111 @@
+
+
+# useSubscription()
+
+Great for keeping resources up-to-date with frequent changes.
+
+When using the default [polling subscriptions](https://dataclient.io/vue/api/PollingSubscription), frequency must be set in
+[Endpoint](https://dataclient.io/rest/api/Endpoint), otherwise will have no effect.
+
+> **Tip**
+>
+> [useLive()](./useLive.md) is a terser way to use in combination with [useSuspense()](./useSuspense.md),
+
+## Usage
+
+```typescript title="api/Price"
+import { RestEndpoint, Entity } from '@data-client/rest';
+
+export class Price extends Entity {
+ symbol = '';
+ price = '0.0';
+ // ...
+
+ pk() {
+ return this.symbol;
+ }
+}
+
+export const getPrice = new RestEndpoint({
+ urlPrefix: 'http://test.com',
+ path: '/price/:symbol',
+ schema: Price,
+ pollFrequency: 5000,
+});
+```
+
+```html title="MasterPrice.vue"
+
+```
+
+## Behavior
+
+> **Tip: Conditional Dependencies**
+>
+> Use `null` as the second argument to any Data Client hook means "do nothing."
+>
+> ```typescript
+> // todo could be undefined if id is undefined
+> const todo = useSubscription(
+> TodoResource.get,
+> computed(() => (id.value ? { id: id.value } : null)),
+> );
+> ```
+
+The subscription is created when the component is set up and removed when it unmounts. When
+an argument passed as a [ref](https://vuejs.org/api/reactivity-core.html#ref) changes, the previous
+subscription is removed and a new one is created for the new arguments.
+
+## Types
+
+```typescript
+function useSubscription(
+ endpoint: ReadEndpoint,
+ ...args: MaybeRefsOrGetters> | [null]
+): void;
+```
+
+Arguments can be plain values, [refs](https://vuejs.org/api/reactivity-core.html#ref) (including [computed](https://vuejs.org/api/reactivity-core.html#computed)), or getter
+functions like `() => ({ id: props.id })`. A plain object like `{ id: props.id }` is read once and won't
+follow prop or route changes, so use a getter or `computed` when an argument can change.
+
+## Examples
+
+### Only subscribe while element is visible
+
+```html title="MasterPrice.vue"
+
+
+
+
{{ price.price }}
+
+```
+
+When `null` is sent as the second argument, the subscription is deactivated. Of course,
+if other components are still subscribed the data updates will still be active.
+
+[useElementVisibility()](https://vueuse.org/core/useElementVisibility/) from VueUse uses [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API), which is very performant. [Template refs](https://vuejs.org/guide/essentials/template-refs.html) allow
+us to access the [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model).
diff --git a/.agents/skills/data-client-vue/references/useSuspense.md b/.agents/skills/data-client-vue/references/useSuspense.md
new file mode 100644
index 000000000000..bdbf0e3e4995
--- /dev/null
+++ b/.agents/skills/data-client-vue/references/useSuspense.md
@@ -0,0 +1,418 @@
+
+
+# useSuspense()
+
+High performance async data rendering without overfetching.
+
+[await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await) `useSuspense()` in Vue components. This means the remainder of the component only runs after the data has loaded, avoiding the complexity of handling loading and error conditions. Instead, fallback handling is
+[centralized](./data-dependency.md#boundaries) with Vue's built-in [Suspense](https://vuejs.org/guide/built-ins/suspense.html).
+
+`useSuspense()` is reactive to data [mutations](./mutations.md); rerendering only when necessary.
+
+## Usage
+
+**Rest**
+
+```typescript title="ProfileResource"
+import { Entity, resource } from '@data-client/rest';
+
+export class Profile extends Entity {
+ id: number | undefined = undefined;
+ avatar = '';
+ fullName = '';
+ bio = '';
+
+ static key = 'Profile';
+}
+
+export const ProfileResource = resource({
+ path: '/profiles/:id',
+ schema: Profile,
+});
+```
+
+```html title="ProfileDetail.vue"
+
+
+
+
+
+```
+
+## Behavior
+
+Cache policy is [Stale-While-Revalidate](https://tools.ietf.org/html/rfc5861) by default but also [configurable](https://dataclient.io/vue/concepts/expiry-policy).
+
+| Expiry Status | Fetch | Suspend | Error | Conditions |
+| ------------- | --------------- | ------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Invalid | yes1 | yes | no | not in store, [deletion](https://dataclient.io/rest/api/resource#delete), [invalidation](./Controller.md#invalidate), [invalidIfStale](https://dataclient.io/vue/concepts/expiry-policy#endpointinvalidifstale) |
+| Stale | yes1 | no | no | (first-render, arg change) & [expiry < now](https://dataclient.io/vue/concepts/expiry-policy) |
+| Valid | no | no | maybe2 | fetch completion |
+| | no | no | no | `null` used as second argument |
+
+> **Note**
+>
+> 1. Identical fetches are automatically deduplicated
+> 2. [Hard errors](https://dataclient.io/vue/concepts/error-policy#hard) to be [caught](./data-dependency.md#async-fallbacks) by [onErrorCaptured()](https://vuejs.org/api/composition-api-lifecycle.html#onerrorcaptured)
+
+> **Tip: Conditional Dependencies**
+>
+> Use `null` as the second argument to any Data Client hook means "do nothing."
+>
+> ```typescript
+> // todo could be undefined if id is undefined
+> const todo = await useSuspense(
+> TodoResource.get,
+> computed(() => (id.value ? { id: id.value } : null)),
+> );
+> ```
+
+## Types
+
+```typescript
+function useSuspense(
+ endpoint: ReadEndpoint,
+ ...args: MaybeRefsOrGetters> | [null]
+): Promise>>>;
+```
+
+Arguments can be plain values, [refs](https://vuejs.org/api/reactivity-core.html#ref) (including [computed](https://vuejs.org/api/reactivity-core.html#computed)), or getter
+functions like `() => ({ id: props.id })`. A plain object like `{ id: props.id }` is read once and won't
+follow prop or route changes, so use a getter or `computed` when an argument can change.
+
+The result updates when the arguments change.
+While data for new arguments loads, the result keeps the previous data instead of becoming `undefined`.
+If that fetch fails, reading the result throws the error (per its [error policy](https://dataclient.io/vue/concepts/error-policy)), so it reaches
+[onErrorCaptured()](https://vuejs.org/api/composition-api-lifecycle.html#onerrorcaptured).
+
+## Examples
+
+### List
+
+```typescript title="ProfileResource"
+import { Entity, resource } from '@data-client/rest';
+
+export class Profile extends Entity {
+ id: number | undefined = undefined;
+ avatar = '';
+ fullName = '';
+ bio = '';
+
+ static key = 'Profile';
+}
+
+export const ProfileResource = resource({
+ path: '/profiles/:id',
+ schema: Profile,
+});
+```
+
+```html title="ProfileList.vue"
+
+
+
+
+
+```
+
+### Sequential
+
+When fetch parameters depend on data from another resource.
+
+```html
+
+```
+
+### Conditional
+
+`null` will avoid binding and fetching data
+
+```ts title="Resources"
+import { Entity, resource } from '@data-client/rest';
+
+export class Post extends Entity {
+ id = 0;
+ userId = 0;
+ title = '';
+ body = '';
+
+ static key = 'Post';
+}
+export const PostResource = resource({
+ path: '/posts/:id',
+ schema: Post,
+});
+
+export class User extends Entity {
+ id = 0;
+ name = '';
+ username = '';
+ email = '';
+ phone = '';
+ website = '';
+
+ get profileImage() {
+ return `https://i.pravatar.cc/64?img=${this.id + 4}`;
+ }
+
+ static key = 'User';
+}
+export const UserResource = resource({
+ urlPrefix: 'https://jsonplaceholder.typicode.com',
+ path: '/users/:id',
+ schema: User,
+});
+```
+
+```html title="PostWithAuthor.vue" {10-16}
+
+
+
+
+
+
+
+```
+
+### Embedded data
+
+When entities are stored in [nested structures](https://dataclient.io/rest/guides/relational-data#nesting), that structure will remain.
+
+```typescript title="api/Post" {12-16}
+export class PaginatedPost extends Entity {
+ id = '';
+ title = '';
+ content = '';
+
+ static key = 'PaginatedPost';
+}
+
+export const getPosts = new RestEndpoint({
+ path: '/post',
+ searchParams: { page: '' },
+ schema: {
+ posts: new Collection([PaginatedPost]),
+ nextPage: '',
+ lastPage: '',
+ },
+});
+```
+
+```html title="ArticleList.vue"
+
+
+
+
+
{{ post.title }}
+
+
+```
diff --git a/docs/core/getting-started/agent-skills.md b/docs/core/getting-started/agent-skills.md
index 9898241ef5b1..2b720b87f76e 100644
--- a/docs/core/getting-started/agent-skills.md
+++ b/docs/core/getting-started/agent-skills.md
@@ -11,7 +11,17 @@ The quickest way to get started is to let an [AI Agent](https://agentskills.io)
## Install
-
+:::react
+
+
+
+:::
+
+:::vue
+
+
+
+:::
Then run skill `/data-client-setup` to install and wire up the provider for your
project. It will automatically detect your framework (NextJS, Expo, React Native, Vue,
@@ -31,13 +41,25 @@ endpoints are found.
and related schemas.
- [**`/data-client-rest`**](https://skills.sh/reactive/data-client/data-client-rest) — defines REST APIs with `resource()`, `RestEndpoint`,
CRUD methods, and response parsing.
+- [**`/data-client-manager`**](https://skills.sh/reactive/data-client/data-client-manager) — implements custom `Manager`s for websockets, SSE,
+ polling, subscriptions, logging, and middleware.
+
+:::react
+
- [**`/data-client-react`**](https://skills.sh/reactive/data-client/data-client-react) — uses `useSuspense`, `useFetch`, `useQuery`, `useLive`,
and mutation hooks.
- [**`/data-client-react-testing`**](https://skills.sh/reactive/data-client/data-client-react-testing) — writes React tests with `renderDataHook`,
fixtures, interceptors, and `nock`.
+
+:::
+
+:::vue
+
+- [**`/data-client-vue`**](https://skills.sh/reactive/data-client/data-client-vue) — uses `useSuspense`, `useFetch`, `useQuery`, `useLive`,
+ and mutation composables with `DataClientPlugin`.
- [**`/data-client-vue-testing`**](https://skills.sh/reactive/data-client/data-client-vue-testing) — writes Vue tests with `renderDataCompose`,
`mountDataClient`, fixtures, and `nock`.
-- [**`/data-client-manager`**](https://skills.sh/reactive/data-client/data-client-manager) — implements custom `Manager`s for websockets, SSE,
- polling, subscriptions, logging, and middleware.
+
+:::
Browse the full catalog at [skills.sh/reactive/data-client](https://skills.sh/reactive/data-client).
diff --git a/docs/core/getting-started/debugging.md b/docs/core/getting-started/debugging.md
index 0ac12e1fd082..2f2ab4996cbd 100644
--- a/docs/core/getting-started/debugging.md
+++ b/docs/core/getting-started/debugging.md
@@ -13,17 +13,9 @@ import useBaseUrl from '@docusaurus/useBaseUrl';
For many debugging tasks, the fastest path is to use an agent that already knows the
:react[`@data-client/react`]:vue[`@data-client/vue`] debugging workflow.
-Install the [`data-client-react` skill](https://skills.sh/reactive/data-client/data-client-react)
+Install the :react[[`data-client-react` skill](https://skills.sh/reactive/data-client/data-client-react)]:vue[[`data-client-vue` skill](https://skills.sh/reactive/data-client/data-client-vue)]
in your coding agent, then ask it to inspect the current page or app state.
-:::vue
-
-The skill is named for React, but its `devtools-debugging` workflow only uses the
-[Controller](../api/Controller.md), so it works the same with `@data-client/vue`. Ask your agent
-to follow that reference.
-
-:::
-
### How agent debugging works
In dev mode, [DevToolsManager](../api/DevToolsManager.md) exposes live `Controller` instances so an agent can inspect
diff --git a/packages/vue/src/test/README.md b/packages/vue/src/test/README.md
index ad7975965d1a..b9a9cd50af35 100644
--- a/packages/vue/src/test/README.md
+++ b/packages/vue/src/test/README.md
@@ -130,7 +130,8 @@ const ArticleComponent = defineComponent({
},
},
async setup(props) {
- const article = await useSuspense(MyResource.get, { id: props.id });
+ // a getter so the fetch follows props.id
+ const article = await useSuspense(MyResource.get, () => ({ id: props.id }));
return () => h('div', article.value.title);
},
});
@@ -269,7 +270,7 @@ interface Interceptor {
6. **Suspense is automatic** - no need to manually wrap components in Suspense
7. **Use reactive props** - Pass a `reactive` in the `props` option and set its members to change component props
8. **Vue Suspense behavior** - Vue's `useSuspense` returns a Promise that suspends when data is missing, then resolves to a ComputedRef
-9. **Reactive props with async setup** - Components using async setup with `useSuspense` that depend on props should use `useFetch` + `watchEffect` for reactive behavior, or rely on non-async setup patterns. Async setup only runs once per component instance.
+9. **Reactive props with async setup** - Async setup runs once per component instance, so pass prop-derived arguments as a getter (`() => ({ id: props.id })`); a plain object is read once and won't follow prop changes.
## Migration from Manual Setup
diff --git a/website/src/components/SkillTabs.tsx b/website/src/components/SkillTabs.tsx
index 8c875822737d..85b406df7070 100644
--- a/website/src/components/SkillTabs.tsx
+++ b/website/src/components/SkillTabs.tsx
@@ -5,17 +5,31 @@ import React from 'react';
interface Props {
repo?: string;
+ /** Directory of the skills within repo; openskills installs one skill per path */
+ skillsDir?: string;
skill?: string;
skills?: string[];
+ /** Skills for the OpenSkills tab when it should differ from `skills` (it has no picker groups) */
+ openSkills?: string[];
}
export default function SkillTabs({
repo = 'reactive/data-client',
+ skillsDir = '.agents/skills',
skill,
skills,
+ openSkills,
}: Props) {
const allSkills = skills ?? (skill ? [skill] : []);
const skillFlag = allSkills.map(s => ` --skill ${s}`).join('');
+ // openskills has no --skill flag; it installs a single skill from its path
+ const openSkillList = openSkills ?? allSkills;
+ const openSkillsCommand =
+ openSkillList.length ?
+ openSkillList
+ .map(s => `npx openskills install ${repo}/${skillsDir}/${s}`)
+ .join('\n')
+ : `npx openskills install ${repo}`;
return (
-
- npx openskills install {repo}
- {skillFlag}
-
+ {openSkillsCommand}
);