From 7f7b1d09fa6128fcac7b8ebbebcd00dcebaa2e8d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 01:25:36 +0000 Subject: [PATCH 1/5] docs: Make Managers concept page work for Vue Manager snippets import from @data-client/core; Sentry, IndexedDB setup and getManagers are split per framework; Coin App demo is React-only. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01XbMoSgP3nZE1bn2HkHpXva --- docs/core/concepts/managers.md | 132 ++++++++++++++++++++++++++++----- 1 file changed, 115 insertions(+), 17 deletions(-) diff --git a/docs/core/concepts/managers.md b/docs/core/concepts/managers.md index 4c13ee7f6b14..ec3465f38ab1 100644 --- a/docs/core/concepts/managers.md +++ b/docs/core/concepts/managers.md @@ -1,5 +1,6 @@ --- title: Centralized side-effect orchestration with React +vue_title: Centralized side-effect orchestration with Vue sidebar_label: Managers and Middleware description: Safe programmatic access to the global store. Enables fully extensible and scalable side-effects. image: /img/social/managers-card.png @@ -30,7 +31,7 @@ sources={{ }} /> -In flux architectures, it is critical all functions in the flux loop are [pure](https://react.dev/learn/keeping-components-pure). +In flux architectures, it is critical all functions in the flux loop are :react[[pure](https://react.dev/learn/keeping-components-pure)]:vue[[pure](https://en.wikipedia.org/wiki/Pure_function)]. Managers provide centralized orchestration of side effects. In other words, they are the means to interface with the world outside Data Client. @@ -78,12 +79,14 @@ export default class LoggingManager implements Manager { Report failed fetches to monitoring services like [Sentry](https://sentry.io) by inspecting [SET_RESPONSE](../api/Actions.md#set_response) actions with `error` set. +:::react + ```typescript import { type Manager, type Middleware, actionTypes, -} from '@data-client/react'; +} from '@data-client/core'; import { captureException } from '@sentry/react'; export default class ErrorReportManager implements Manager { @@ -99,6 +102,33 @@ export default class ErrorReportManager implements Manager { } ``` +::: + +:::vue + +```typescript +import { + type Manager, + type Middleware, + actionTypes, +} from '@data-client/core'; +import { captureException } from '@sentry/vue'; + +export default class ErrorReportManager implements Manager { + middleware: Middleware = controller => next => async action => { + if (action.type === actionTypes.SET_RESPONSE && action.error) + captureException(action.response, { + extra: { endpoint: action.endpoint.name, args: action.args }, + }); + return next(action); + }; + + cleanup() {} +} +``` + +::: + ### Metrics {#metrics} Track fetch timing by observing [FETCH](../api/Actions.md#fetch) actions. `action.meta.promise` @@ -109,7 +139,7 @@ import { type Manager, type Middleware, actionTypes, -} from '@data-client/react'; +} from '@data-client/core'; import { trackTiming } from './analytics'; export default class MetricsManager implements Manager { @@ -136,7 +166,7 @@ import { type Manager, type Middleware, actionTypes, -} from '@data-client/react'; +} from '@data-client/core'; import { toast } from './toast'; export default class ToastManager implements Manager { @@ -162,7 +192,7 @@ triggering refetch of any _actively rendered_ data without suspending ([stale-wh [init()](../api/Manager.md#init) and [cleanup()](../api/Manager.md#cleanup) manage the event listeners. ```typescript -import type { Manager, Middleware, Controller } from '@data-client/react'; +import type { Manager, Middleware, Controller } from '@data-client/core'; export default class RefreshManager implements Manager { declare protected controller: Controller; @@ -196,7 +226,7 @@ import { type Manager, type Middleware, actionTypes, -} from '@data-client/react'; +} from '@data-client/core'; export default class TabSyncManager implements Manager { protected channel = new BroadcastChannel('data-client'); @@ -225,14 +255,14 @@ export default class TabSyncManager implements Manager { Persist the store with [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) (here via [idb-keyval](https://www.npmjs.com/package/idb-keyval)); restore it with -:react[[DataProvider's initialState](../api/DataProvider.md#initialState)]:vue[DataClientPlugin's `initialState` option]. IndexedDB writes are +:react[[DataProvider's initialState](../api/DataProvider.md#initialState)]:vue[[DataClientPlugin's `initialState` option](../getting-started/installation.md#add-provider-at-top-level-component)]. IndexedDB writes are asynchronous and use [structured clone](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) instead of blocking the main thread with JSON serialization like `localStorage` would. Debouncing writes keeps rapid action bursts cheap. Consider [expiry times](./expiry-policy.md) when restoring. ```typescript -import type { Manager, Middleware } from '@data-client/react'; +import type { Manager, Middleware } from '@data-client/core'; import { set } from 'idb-keyval'; export default class PersistManager implements Manager { @@ -255,9 +285,16 @@ export default class PersistManager implements Manager { } ``` -```tsx +:::react + +```tsx title="index.tsx" +import { DataProvider, getDefaultManagers } from '@data-client/react'; +import { createRoot } from 'react-dom/client'; import { get } from 'idb-keyval'; +import App from './App'; +import PersistManager from './PersistManager'; +const managers = [...getDefaultManagers(), new PersistManager()]; const initialState = await get('data-client'); createRoot(document.body).render( @@ -267,6 +304,27 @@ createRoot(document.body).render( ); ``` +::: + +:::vue + +```ts title="main.ts" +import { createApp } from 'vue'; +import { DataClientPlugin, getDefaultManagers } from '@data-client/vue'; +import { get } from 'idb-keyval'; +import App from './App.vue'; +import PersistManager from './PersistManager'; + +const managers = [...getDefaultManagers(), new PersistManager()]; +const initialState = await get('data-client'); + +const app = createApp(App); +app.use(DataClientPlugin, { initialState, managers }); +app.mount('#app'); +``` + +::: + ### Middleware data stream (push-based) {#data-stream} Adding a manager to process data pushed from the server by [websockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) @@ -275,7 +333,7 @@ we can maintain fresh data when the data updates are independent of user action. price, or a real-time collaborative editor. ```typescript -import { type Manager, type Middleware, Controller } from '@data-client/react'; +import type { Manager, Middleware, Controller } from '@data-client/core'; import type { Entity } from '@data-client/rest'; export default class StreamManager implements Manager { @@ -286,7 +344,7 @@ export default class StreamManager implements Manager { constructor( createEventSource: () => WebSocket | EventSource, - entities: Record, + entities: Record, ) { this.createEventSource = createEventSource; this.entities = entities; @@ -295,7 +353,7 @@ export default class StreamManager implements Manager { middleware: Middleware = controller => { this.controller = controller; return next => async action => next(action); - } + }; connect() { this.evtSource = this.createEventSource(); @@ -304,7 +362,11 @@ export default class StreamManager implements Manager { try { const msg = JSON.parse(event.data); if (msg.type in this.entities) - this.controller.set(this.entities[msg.type], ...msg.args, msg.data); + this.controller.set( + this.entities[msg.type], + ...msg.args, + msg.data, + ); } catch (e) { console.error('Failed to handle message'); console.error(e); @@ -378,6 +440,8 @@ When using WebSockets or other real-time data sources, you may want to skip logg certain high-frequency actions to [DevToolsManager](../api/DevToolsManager.md) to avoid overwhelming the browser extension. +:::react + ```typescript import { getDefaultManagers, actionTypes } from '@data-client/react'; import StreamManager from './StreamManager'; @@ -385,10 +449,38 @@ import { Ticker } from './Ticker'; export default function getManagers() { return [ - new StreamManager( - () => new WebSocket('wss://ws-feed.example.com'), - { ticker: Ticker }, - ), + new StreamManager(() => new WebSocket('wss://ws-feed.example.com'), { + ticker: Ticker, + }), + ...getDefaultManagers({ + devToolsManager: { + // Increase latency buffer for high-frequency updates + latency: 1000, + // Skip WebSocket SET actions to avoid log spam + // (batched writes use the [Ticker] schema) + predicate: (state, action) => + action.type !== actionTypes.SET || + (action.schema !== Ticker && action.schema[0] !== Ticker), + }, + }), + ]; +} +``` + +::: + +:::vue + +```typescript +import { getDefaultManagers, actionTypes } from '@data-client/vue'; +import StreamManager from './StreamManager'; +import { Ticker } from './Ticker'; + +export default function getManagers() { + return [ + new StreamManager(() => new WebSocket('wss://ws-feed.example.com'), { + ticker: Ticker, + }), ...getDefaultManagers({ devToolsManager: { // Increase latency buffer for high-frequency updates @@ -404,6 +496,12 @@ export default function getManagers() { } ``` +::: + +:::react + ### Coin App + +::: From ece17259f211eeadf65c6114feaeb32cd3aef769 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 09:11:47 +0000 Subject: [PATCH 2/5] docs: Import framework-agnostic manager types from core StreamManager uses EntityInterface from @data-client/core like Manager.md, and Manager.md's manager examples import from core so they read right on Vue. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01XbMoSgP3nZE1bn2HkHpXva --- docs/core/api/Manager.md | 21 +++++++++++++-------- docs/core/concepts/managers.md | 12 ++++++++---- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/docs/core/api/Manager.md b/docs/core/api/Manager.md index b69e343a3165..86cbdbdb2caf 100644 --- a/docs/core/api/Manager.md +++ b/docs/core/api/Manager.md @@ -252,7 +252,7 @@ import type { Manager, Middleware } from '@data-client/core'; import CurrentTime from './CurrentTime'; export default class TimeManager implements Manager { - protected declare intervalID?: ReturnType; + declare protected intervalID?: ReturnType; middleware: Middleware = controller => { this.intervalID = setInterval(() => { @@ -277,8 +277,8 @@ export default class TimeManager implements Manager { ```ts -import type { Manager, Middleware } from '@data-client/react'; -import { actionTypes } from '@data-client/react'; +import type { Manager, Middleware } from '@data-client/core'; +import { actionTypes } from '@data-client/core'; export default class LoggingManager implements Manager { middleware: Middleware = controller => next => async action => { @@ -321,19 +321,24 @@ In case we want to 'handle' a certain [action](./Actions.md), we can 'consume' i ```ts title="isEntity" collapsed import type { Schema, EntityInterface } from '@data-client/core'; -export default function isEntity(schema: Schema): schema is EntityInterface { +export default function isEntity( + schema: Schema, +): schema is EntityInterface { return schema !== null && (schema as any).pk !== undefined; } ``` - ```ts title="SubsManager" -import type { Manager, Middleware, EntityInterface } from '@data-client/react'; -import { actionTypes } from '@data-client/react'; +import type { + Manager, + Middleware, + EntityInterface, +} from '@data-client/core'; +import { actionTypes } from '@data-client/core'; import isEntity from './isEntity'; export default class CustomSubsManager implements Manager { - protected declare entities: Record; + declare protected entities: Record; middleware: Middleware = controller => next => async action => { switch (action.type) { diff --git a/docs/core/concepts/managers.md b/docs/core/concepts/managers.md index ec3465f38ab1..7331726be981 100644 --- a/docs/core/concepts/managers.md +++ b/docs/core/concepts/managers.md @@ -333,18 +333,22 @@ we can maintain fresh data when the data updates are independent of user action. price, or a real-time collaborative editor. ```typescript -import type { Manager, Middleware, Controller } from '@data-client/core'; -import type { Entity } from '@data-client/rest'; +import type { + Manager, + Middleware, + Controller, + EntityInterface, +} from '@data-client/core'; export default class StreamManager implements Manager { declare protected controller: Controller; declare protected evtSource: WebSocket; // | EventSource; declare protected createEventSource: () => WebSocket | EventSource; - declare protected entities: Record; + declare protected entities: Record; constructor( createEventSource: () => WebSocket | EventSource, - entities: Record, + entities: Record, ) { this.createEventSource = createEventSource; this.entities = entities; From 1028f07e8984f1b3f595280a7c5923cad07e054d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 21:33:44 +0000 Subject: [PATCH 3/5] docs: Import from react/vue in examples, never core Apps install @data-client/react or @data-client/vue; core is only a transitive dependency, so importing it breaks under strict package managers. Code fences marked framework-imports show @data-client/vue on Vue pages, so manager examples stay single-sourced. The Playground editor gets @data-client/vue types so those blocks resolve on Vue pages. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01XbMoSgP3nZE1bn2HkHpXva --- .../skills/packages-documentation/SKILL.md | 3 + docs/core/api/Controller.md | 13 +- docs/core/api/Manager.md | 22 +-- docs/core/concepts/managers.md | 67 ++----- docs/core/guides/render-as-you-fetch.md | 2 +- scripts/copywebsitetypes.sh | 1 + website/framework-docs/README.md | 5 + website/framework-docs/remarkFramework.js | 18 ++ .../editor-types/@data-client/vue.d.ts | 181 ++++++++++++++++++ .../components/Playground/monaco/typeLibs.ts | 1 + 10 files changed, 246 insertions(+), 67 deletions(-) create mode 100644 website/src/components/Playground/editor-types/@data-client/vue.d.ts diff --git a/.agents/skills/packages-documentation/SKILL.md b/.agents/skills/packages-documentation/SKILL.md index 42a5bafe1a2e..6f415e872c37 100644 --- a/.agents/skills/packages-documentation/SKILL.md +++ b/.agents/skills/packages-documentation/SKILL.md @@ -29,6 +29,9 @@ Wrap framework-specific content instead, so shared prose and resources stay in o - `:react[...]` / `:vue[...]` for inline words or links - `` instead of ``, with shared resource code blocks directly inside and component code blocks in `:::react` / `:::vue` +- `framework-imports` on a code fence for framework-agnostic code (managers, middleware): write + `@data-client/react` imports and Vue pages show `@data-client/vue`. Examples never import + `@data-client/core`; apps only install the react or vue package - `vue_title:`, `vue_description:` (any `vue_:`) front matter overrides - `frameworks: [react]` front matter for pages with no Vue equivalent - `foo.vue.md` replaces `foo.md` for Vue; only for Vue-only pages or when nothing is shareable diff --git a/docs/core/api/Controller.md b/docs/core/api/Controller.md index 4080d7c5c36d..2f36d963bf55 100644 --- a/docs/core/api/Controller.md +++ b/docs/core/api/Controller.md @@ -540,7 +540,7 @@ import { type Queryable, type SchemaArgs, type DenormalizeNullable, -} from '@data-client/core'; +} from '@data-client/react'; /** Oversimplified useQuery */ function useQuery( @@ -609,7 +609,7 @@ import { useController, StateContext, EndpointInterface, -} from '@data-client/core'; +} from '@data-client/react'; /** Oversimplified useCache */ function useCache( @@ -622,9 +622,12 @@ function useCache( } ``` -```tsx title="MyManager.ts" -import type { Manager, Middleware, actionTypes } from '@data-client/core'; -import type { EndpointInterface } from '@data-client/endpoint'; +```tsx title="MyManager.ts" framework-imports +import { + type Manager, + type Middleware, + actionTypes, +} from '@data-client/react'; export default class MyManager implements Manager { middleware: Middleware = controller => { diff --git a/docs/core/api/Manager.md b/docs/core/api/Manager.md index a757e0cd28f0..f641b6070fcd 100644 --- a/docs/core/api/Manager.md +++ b/docs/core/api/Manager.md @@ -239,7 +239,7 @@ The job of `middleware` is to dispatch actions, respond to [actions](./Actions.m ```ts title="CurrentTime" collapsed -import { Entity } from '@data-client/endpoint'; +import { Entity } from '@data-client/rest'; export default class CurrentTime extends Entity { id = 0; @@ -247,8 +247,8 @@ export default class CurrentTime extends Entity { } ``` -```ts title="TimeManager" -import type { Manager, Middleware } from '@data-client/core'; +```ts title="TimeManager" framework-imports +import type { Manager, Middleware } from '@data-client/react'; import CurrentTime from './CurrentTime'; export default class TimeManager implements Manager { @@ -276,9 +276,9 @@ export default class TimeManager implements Manager { -```ts -import type { Manager, Middleware } from '@data-client/core'; -import { actionTypes } from '@data-client/core'; +```ts framework-imports +import type { Manager, Middleware } from '@data-client/react'; +import { actionTypes } from '@data-client/react'; export default class LoggingManager implements Manager { middleware: Middleware = controller => next => async action => { @@ -318,8 +318,8 @@ In case we want to 'handle' a certain [action](./Actions.md), we can 'consume' i -```ts title="isEntity" collapsed -import type { Schema, EntityInterface } from '@data-client/core'; +```ts title="isEntity" collapsed framework-imports +import type { Schema, EntityInterface } from '@data-client/react'; export default function isEntity( schema: Schema, @@ -328,13 +328,13 @@ export default function isEntity( } ``` -```ts title="SubsManager" +```ts title="SubsManager" framework-imports import type { Manager, Middleware, EntityInterface, -} from '@data-client/core'; -import { actionTypes } from '@data-client/core'; +} from '@data-client/react'; +import { actionTypes } from '@data-client/react'; import isEntity from './isEntity'; export default class CustomSubsManager implements Manager { diff --git a/docs/core/concepts/managers.md b/docs/core/concepts/managers.md index 7331726be981..4f39d751b7f6 100644 --- a/docs/core/concepts/managers.md +++ b/docs/core/concepts/managers.md @@ -60,8 +60,8 @@ its [Controller](../api/Controller.md) ### Middleware logging -```typescript -import type { Manager, Middleware } from '@data-client/core'; +```typescript framework-imports +import type { Manager, Middleware } from '@data-client/react'; export default class LoggingManager implements Manager { middleware: Middleware = controller => next => async action => { @@ -86,7 +86,7 @@ import { type Manager, type Middleware, actionTypes, -} from '@data-client/core'; +} from '@data-client/react'; import { captureException } from '@sentry/react'; export default class ErrorReportManager implements Manager { @@ -111,7 +111,7 @@ import { type Manager, type Middleware, actionTypes, -} from '@data-client/core'; +} from '@data-client/vue'; import { captureException } from '@sentry/vue'; export default class ErrorReportManager implements Manager { @@ -134,12 +134,12 @@ export default class ErrorReportManager implements Manager { Track fetch timing by observing [FETCH](../api/Actions.md#fetch) actions. `action.meta.promise` resolves when the fetch completes. -```typescript +```typescript framework-imports import { type Manager, type Middleware, actionTypes, -} from '@data-client/core'; +} from '@data-client/react'; import { trackTiming } from './analytics'; export default class MetricsManager implements Manager { @@ -161,12 +161,12 @@ export default class MetricsManager implements Manager { Show a toast when any [mutation](/rest/guides/side-effects) succeeds or fails. -```typescript +```typescript framework-imports import { type Manager, type Middleware, actionTypes, -} from '@data-client/core'; +} from '@data-client/react'; import { toast } from './toast'; export default class ToastManager implements Manager { @@ -191,8 +191,8 @@ export default class ToastManager implements Manager { triggering refetch of any _actively rendered_ data without suspending ([stale-while-revalidate](./expiry-policy.md)). [init()](../api/Manager.md#init) and [cleanup()](../api/Manager.md#cleanup) manage the event listeners. -```typescript -import type { Manager, Middleware, Controller } from '@data-client/core'; +```typescript framework-imports +import type { Manager, Middleware, Controller } from '@data-client/react'; export default class RefreshManager implements Manager { declare protected controller: Controller; @@ -221,12 +221,12 @@ export default class RefreshManager implements Manager { When a mutation succeeds in one tab, mark data stale in all other tabs using [BroadcastChannel](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel). -```typescript +```typescript framework-imports import { type Manager, type Middleware, actionTypes, -} from '@data-client/core'; +} from '@data-client/react'; export default class TabSyncManager implements Manager { protected channel = new BroadcastChannel('data-client'); @@ -261,8 +261,8 @@ instead of blocking the main thread with JSON serialization like `localStorage` Debouncing writes keeps rapid action bursts cheap. Consider [expiry times](./expiry-policy.md) when restoring. -```typescript -import type { Manager, Middleware } from '@data-client/core'; +```typescript framework-imports +import type { Manager, Middleware } from '@data-client/react'; import { set } from 'idb-keyval'; export default class PersistManager implements Manager { @@ -332,13 +332,13 @@ or [Server Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server- we can maintain fresh data when the data updates are independent of user action. For example, a trading app's price, or a real-time collaborative editor. -```typescript +```typescript framework-imports import type { Manager, Middleware, Controller, EntityInterface, -} from '@data-client/core'; +} from '@data-client/react'; export default class StreamManager implements Manager { declare protected controller: Controller; @@ -444,9 +444,7 @@ When using WebSockets or other real-time data sources, you may want to skip logg certain high-frequency actions to [DevToolsManager](../api/DevToolsManager.md) to avoid overwhelming the browser extension. -:::react - -```typescript +```typescript framework-imports import { getDefaultManagers, actionTypes } from '@data-client/react'; import StreamManager from './StreamManager'; import { Ticker } from './Ticker'; @@ -471,37 +469,6 @@ export default function getManagers() { } ``` -::: - -:::vue - -```typescript -import { getDefaultManagers, actionTypes } from '@data-client/vue'; -import StreamManager from './StreamManager'; -import { Ticker } from './Ticker'; - -export default function getManagers() { - return [ - new StreamManager(() => new WebSocket('wss://ws-feed.example.com'), { - ticker: Ticker, - }), - ...getDefaultManagers({ - devToolsManager: { - // Increase latency buffer for high-frequency updates - latency: 1000, - // Skip WebSocket SET actions to avoid log spam - // (batched writes use the [Ticker] schema) - predicate: (state, action) => - action.type !== actionTypes.SET || - (action.schema !== Ticker && action.schema[0] !== Ticker), - }, - }), - ]; -} -``` - -::: - :::react ### Coin App diff --git a/docs/core/guides/render-as-you-fetch.md b/docs/core/guides/render-as-you-fetch.md index c054251cd4e7..d41b64324e4a 100644 --- a/docs/core/guides/render-as-you-fetch.md +++ b/docs/core/guides/render-as-you-fetch.md @@ -39,7 +39,7 @@ Use [Controller.fetchIfStale](../api/Controller#fetchIfStale) in the route event ```ts -import { Controller } from '@data-client/core'; +import { Controller } from '@data-client/react'; import { lazy, Route } from '@anansi/router'; import { getImage } from '@data-client/img'; diff --git a/scripts/copywebsitetypes.sh b/scripts/copywebsitetypes.sh index ec58c61dfe77..6aabd3ed480b 100755 --- a/scripts/copywebsitetypes.sh +++ b/scripts/copywebsitetypes.sh @@ -4,6 +4,7 @@ cp ./packages/graphql/index.d.ts ./website/src/components/Playground/editor-type cp ./packages/normalizr/index.d.ts ./website/src/components/Playground/editor-types/@data-client/normalizr.d.ts cp ./packages/react/index.d.ts ./website/src/components/Playground/editor-types/@data-client/react.d.ts cp ./packages/rest/index.d.ts ./website/src/components/Playground/editor-types/@data-client/rest.d.ts +cp ./packages/vue/index.d.ts ./website/src/components/Playground/editor-types/@data-client/vue.d.ts mkdir -p ./website/src/components/Playground/editor-types/@data-client/rest mkdir -p ./website/src/components/Playground/editor-types/@data-client/core mkdir -p ./website/src/components/Playground/editor-types/@data-client/react diff --git a/website/framework-docs/README.md b/website/framework-docs/README.md index a320c6100821..9e5572eb0927 100644 --- a/website/framework-docs/README.md +++ b/website/framework-docs/README.md @@ -55,12 +55,17 @@ Errors are caught by :react[[Error Boundaries](./AsyncBoundary.md)]:vue[`onError | -------------------------------------- | --------------------------------------------- | | Different block of content | `:::react` / `:::vue` | | Different word or link inline | `:react[...]` / `:vue[...]` | +| Same code, framework's package | ` ```ts framework-imports ` (see below) | | Different front matter value | `vue_:` overrides `:` | | Different sidebar category value | `"vue_"` overrides `""` | | Different heading text | `## :react[...]:vue[...] {#stable-id}` | | Page has no Vue equivalent | `frameworks: [react]` in front matter | | Vue-only page, or nothing is shareable | `foo.vue.md` next to (or instead of) `foo.md` | +Framework-agnostic code (managers, middleware, types) imports from `@data-client/react` and adds +`framework-imports` to the fence; Vue pages show `@data-client/vue` instead. Never import +`@data-client/core` in examples: apps only install `@data-client/react` or `@data-client/vue`. + Nest inside an admonition by giving the outer one more colons (`::::tip` ... `::::`). Sidebars come from `website/sidebars.json` for both frameworks; entries for docs that don't exist diff --git a/website/framework-docs/remarkFramework.js b/website/framework-docs/remarkFramework.js index 96c53a894246..f2ab6f7c1c5e 100644 --- a/website/framework-docs/remarkFramework.js +++ b/website/framework-docs/remarkFramework.js @@ -4,6 +4,8 @@ * * Block: :::react ... ::: or :::vue ... ::: * Inline: :react[Suspense boundary] / :vue[] + * Code: ```ts framework-imports (imports of '@data-client/react' become + * '@data-client/vue' on Vue pages) * * Matching blocks are unwrapped, the rest are removed. Runs per docs instance, * so the same source file renders once for React and once for Vue. @@ -35,6 +37,21 @@ function filterChildren(node, framework) { }); } +const FRAMEWORK_IMPORTS = /(^|\s)framework-imports(?=\s|$)/; +const REACT_PACKAGE = /(['"])@data-client\/react\1/g; + +/** Point framework-agnostic code (managers, middleware) at this framework's package */ +function rewriteImports(node, framework) { + if (node.type === 'code' && FRAMEWORK_IMPORTS.test(node.meta ?? '')) { + node.meta = node.meta.replace(FRAMEWORK_IMPORTS, '$1').trim() || null; + node.value = node.value.replace( + REACT_PACKAGE, + `$1@data-client/${framework}$1`, + ); + } + node.children?.forEach(child => rewriteImports(child, framework)); +} + const DOCS_LINK = /^\/docs(?:\/([^#?]*))?([#?].*)?$/; function rewriteLinks(node, routeBasePath, docIds) { @@ -54,6 +71,7 @@ module.exports = function remarkFramework({ }) { return tree => { filterChildren(tree, framework); + rewriteImports(tree, framework); if (routeBasePath) rewriteLinks(tree, routeBasePath, docIds); }; }; diff --git a/website/src/components/Playground/editor-types/@data-client/vue.d.ts b/website/src/components/Playground/editor-types/@data-client/vue.d.ts new file mode 100644 index 000000000000..bd1e1f3238ed --- /dev/null +++ b/website/src/components/Playground/editor-types/@data-client/vue.d.ts @@ -0,0 +1,181 @@ +import { EndpointInterface, FetchFunction, Schema, ResolveType, Denormalize, DenormalizeNullable, Queryable, SchemaArgs, ErrorTypes, Controller, DevToolsManager, DevToolsConfig, NetworkManager, SubscriptionManager, Manager, State, GCInterface } from '@data-client/core'; +export { AbstractInstanceType, ActionTypes, Controller, CreateCountRef, DataClientDispatch, DefaultConnectionListener, Denormalize, DenormalizeNullable, DevToolsManager, Dispatch, EndpointExtraOptions, EndpointInterface, EntityInterface, ErrorTypes, ExpiryStatus, FetchAction, FetchFunction, GCInterface, GCOptions, GenericDispatch, InvalidateAction, LogoutManager, Manager, Middleware, MiddlewareAPI, NetworkError, NetworkManager, Normalize, NormalizeNullable, PK, PollingSubscription, Queryable, ResetAction, ResolveType, Schema, SchemaArgs, SchemaClass, SetAction, SetResponseAction, State, SubscribeAction, SubscriptionManager, UnknownError, UnsubscribeAction, UpdateFunction, actionTypes } from '@data-client/core'; +import { MaybeRefOrGetter, DeepReadonly, ComputedRef, Ref, App, ShallowRef } from 'vue'; + +/** Maps each parameter to accept raw value, Ref, ComputedRef, or getter */ +type MaybeRefsOrGetters = { + readonly [K in keyof T]: MaybeRefOrGetter; +}; +/** Maps each parameter to accept raw value, Ref, ComputedRef, or getter, with nullable support */ +type MaybeRefsOrGettersNullable = { + readonly [K in keyof T]: MaybeRefOrGetter; +}; + +/** + * Ensure an endpoint is available. + * Suspends until it is. + * + * @see https://dataclient.io/docs/api/useSuspense + * @throws {Promise} If data is not yet available. + * @throws {NetworkError} If fetch fails. + */ +declare function useSuspense>(endpoint: E, ...args: MaybeRefsOrGetters>): Promise : Denormalize>>>; +declare function useSuspense>(endpoint: E, ...args: MaybeRefsOrGettersNullable> | readonly [null]): Promise | undefined : DenormalizeNullable>>>; + +/** + * Keeps a resource fresh by subscribing to updates. + * Mirrors React hook API. Pass `null` as first arg to unsubscribe. + * @see https://dataclient.io/docs/api/useSubscription + */ +declare function useSubscription>(endpoint: E, ...args: MaybeRefsOrGettersNullable> | readonly [null]): void; + +/** + * Query the store. + * + * `useQuery` results are globally memoized. + * @see https://dataclient.io/docs/api/useQuery + */ +declare function useQuery(schema: S, ...args: MaybeRefsOrGetters>): ComputedRef | undefined>; + +/** + * Ensure an endpoint is available. Keeps it fresh once it is. + * + * useSuspense() + useSubscription() + * @see https://dataclient.io/docs/api/useLive + * @throws {Promise} If data is not yet available. + * @throws {NetworkError} If fetch fails. + */ +declare function useLive>(endpoint: E, ...args: MaybeRefsOrGetters>): Promise : Denormalize>>>; +declare function useLive>(endpoint: E, ...args: MaybeRefsOrGettersNullable> | readonly [null]): Promise | undefined : DenormalizeNullable>>>; + +/** + * Takes an async function and tracks resolution as a boolean. + * + * @see https://dataclient.io/docs/api/useLoading + * @param func A function returning a promise + * @example + ``` + function Button({ onClick, children, ...props }) { + const [clickHandler, loading] = useLoading(onClick); + return h('button', { onClick: clickHandler, ...props }, + loading.value ? 'Loading...' : children + ); + } + ``` + */ +declare function useLoading Promise>(func: F): [F, Ref, Ref]; + +/** + * Keeps value updated after delay time + * + * @see https://dataclient.io/docs/api/useDebounce + * @param value Any immutable value (can be a ref) + * @param delay Time in milliseconds to wait til updating the value + * @param updatable Whether to update at all + * @example + ``` + const [debouncedQuery, isPending] = useDebounce(query, 200); + const list = useSuspense(getThings, { query: debouncedQuery.value }); + ``` + */ +declare function useDebounce(value: T | Ref, delay: number, updatable?: boolean | Ref): [Ref, Ref]; + +type FetchPromise = Promise & { + resolved: boolean; +}; +/** + * Fetch an Endpoint if it is not in cache or stale. + * @see https://dataclient.io/docs/api/useFetch + */ +declare function useFetch>(endpoint: E, ...args: MaybeRefsOrGetters>): Readonly : Denormalize>>>; +declare function useFetch>(endpoint: E, ...args: MaybeRefsOrGettersNullable> | readonly [null]): Readonly : DenormalizeNullable> | undefined>>; + +/** + * Read an Endpoint's response if it is ready. + * + * `useCache` is globally memoized. + * @see https://dataclient.io/docs/api/useCache + */ +declare function useCache, 'key' | 'schema' | 'invalidIfStale'>>(endpoint: E, ...args: MaybeRefsOrGetters>): ComputedRef any ? ResolveType | undefined : any : DenormalizeNullable>; +declare function useCache, 'key' | 'schema' | 'invalidIfStale'>>(endpoint: E, ...args: MaybeRefsOrGettersNullable> | readonly [null]): ComputedRef any ? ResolveType | undefined : any : DenormalizeNullable>; + +/** + * Use async data with { data, loading, error } (DLE) + * @see https://dataclient.io/docs/api/useDLE + */ +declare function useDLE>(endpoint: E, ...args: MaybeRefsOrGetters>): { + data: ComputedRef | undefined : Denormalize | DenormalizeNullable>; + loading: ComputedRef; + error: ComputedRef; +}; +declare function useDLE>(endpoint: E, ...args: MaybeRefsOrGettersNullable> | readonly [null]): { + data: ComputedRef>; + loading: ComputedRef; + error: ComputedRef; +}; + +declare function useController(): Controller; + +/** Returns the default Managers used by DataProvider. + * + * @see https://dataclient.io/docs/api/getDefaultManagers + */ +declare let getDefaultManagers: (options?: GetManagersOptions) => Manager[]; + +type GetManagersOptions = { + devToolsManager?: DevToolsManager | DevToolsConfig | null; + networkManager?: NetworkManager | ConstructorArgs | null; + subscriptionManager?: SubscriptionManager | ConstructorArgs | null; +}; +type ConstructorArgs = T extends new (options: infer O) => any ? O : never; + +interface ProvideOptions { + managers?: Manager[]; + initialState?: State; + Controller?: typeof Controller; + gcPolicy?: GCInterface; + app?: App; +} +interface ProvidedDataClient { + controller: InstanceType; + /** Optimistic overlay state ref provided to consumers */ + stateRef: ShallowRef>; + /** Start the provider (called on mount) */ + start: () => void; + /** Stop the provider (called on unmount) */ + stop: () => void; +} +/** + * Core provider logic that can be used by both composable and plugin. + * This function handles the actual setup of the data client without Vue-specific concerns. + */ +declare function createDataClient(options?: ProvideOptions): ProvidedDataClient; + +/** + * Vue 3 Plugin for Reactive Data Client + * + * Usage: + * ```ts + * import { createApp } from 'vue'; + * import { DataClientPlugin } from '@data-client/vue'; + * + * const app = createApp(App); + * app.use(DataClientPlugin, { + * managers: getDefaultManagers(), + * initialState: customInitialState, + * }); + * app.mount('#app'); + * ``` + */ +declare const DataClientPlugin: { + install(app: App, options?: ProvideOptions): ProvidedDataClient; +}; +declare module 'vue' { + interface ComponentCustomProperties { + $dataClient: Controller; + } +} + +export { DataClientPlugin, type MaybeRefsOrGetters, type MaybeRefsOrGettersNullable, type ProvideOptions, type ProvidedDataClient, createDataClient, getDefaultManagers, useCache, useController, useDLE, useDebounce, useFetch, useLive, useLoading, useQuery, useSubscription, useSuspense }; diff --git a/website/src/components/Playground/monaco/typeLibs.ts b/website/src/components/Playground/monaco/typeLibs.ts index e70ba8a9afff..da9b41e102fb 100644 --- a/website/src/components/Playground/monaco/typeLibs.ts +++ b/website/src/components/Playground/monaco/typeLibs.ts @@ -77,6 +77,7 @@ const DATA_CLIENT_ENTRIES = [ 'core/next', 'core', 'react', + 'vue', 'endpoint', 'normalizr', 'graphql', From c84c5e3ef33190fc396b03b338096893aa59b390 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 21:37:56 +0000 Subject: [PATCH 4/5] docs: Single-source StreamManager and DevTools predicate examples Collapse the README StreamManager react/vue pair with framework-imports (and fix its typeof EntityInterface), flag the DevTools predicate example, keep the oversimplified useQuery/useCache hooks React-only, and drop the last @data-client/core import from resource.md. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01XbMoSgP3nZE1bn2HkHpXva --- docs/core/README.md | 49 ++------------------------- docs/core/api/Controller.md | 8 +++++ docs/core/api/DevToolsManager.md | 2 +- docs/core/getting-started/resource.md | 7 ++-- 4 files changed, 16 insertions(+), 50 deletions(-) diff --git a/docs/core/README.md b/docs/core/README.md index 0fb7697d35fa..a403069fd5ed 100644 --- a/docs/core/README.md +++ b/docs/core/README.md @@ -663,16 +663,14 @@ which can be used to [initiate data updates](./concepts/managers.md#data-stream)
StreamManager -:::react - -```typescript +```typescript framework-imports import type { Manager, Middleware, ActionTypes } from '@data-client/react'; import { Controller, actionTypes } from '@data-client/react'; import type { EntityInterface } from '@data-client/rest'; export default class StreamManager implements Manager { declare protected evtSource: WebSocket | EventSource; - declare protected entities: Record; + declare protected entities: Record; constructor( evtSource: WebSocket | EventSource, @@ -702,49 +700,6 @@ export default class StreamManager implements Manager { } ``` -::: - -:::vue - -```typescript -import type { Manager, Middleware, ActionTypes } from '@data-client/vue'; -import { Controller, actionTypes } from '@data-client/vue'; -import type { EntityInterface } from '@data-client/rest'; - -export default class StreamManager implements Manager { - declare protected evtSource: WebSocket | EventSource; - declare protected entities: Record; - - constructor( - evtSource: WebSocket | EventSource, - entities: Record, - ) { - this.evtSource = evtSource; - this.entities = entities; - } - - middleware: Middleware = controller => { - this.evtSource.onmessage = event => { - try { - const msg = JSON.parse(event.data); - if (msg.type in this.endpoints) - controller.set(this.entities[msg.type], ...msg.args, msg.data); - } catch (e) { - console.error('Failed to handle message'); - console.error(e); - } - }; - return next => async action => next(action); - }; - - cleanup() { - this.evtSource.close(); - } -} -``` - -::: -
If we don't want the full data stream, we can [useSubscription()](./api/useSubscription.md) or [useLive()](./api/useLive.md) diff --git a/docs/core/api/Controller.md b/docs/core/api/Controller.md index 2f36d963bf55..7e3c8b47c28e 100644 --- a/docs/core/api/Controller.md +++ b/docs/core/api/Controller.md @@ -533,6 +533,8 @@ Looks up any [Queryable](/rest/api/schema#queryable) [Schema](/rest/api/schema#s This is used in [useQuery](./useQuery.md) and can be used in [Managers](./Manager.md) to safely access the store. +:::react + ```tsx title="useQuery.ts" import { useController, @@ -554,6 +556,8 @@ function useQuery( } ``` +::: + ### getResponse(endpoint, ...args, state) {#getResponse} ```ts title="returns" @@ -604,6 +608,8 @@ A number representing time when it expires. Compare to Date.now(). This is used in [useCache](./useCache.md), [useSuspense](./useSuspense.md) and can be used in [Managers](./Manager.md) to lookup a response with the state provided. +:::react + ```tsx title="useCache.ts" import { useController, @@ -622,6 +628,8 @@ function useCache( } ``` +::: + ```tsx title="MyManager.ts" framework-imports import { type Manager, diff --git a/docs/core/api/DevToolsManager.md b/docs/core/api/DevToolsManager.md index 7f2194ed06b1..4f209e4fefce 100644 --- a/docs/core/api/DevToolsManager.md +++ b/docs/core/api/DevToolsManager.md @@ -86,7 +86,7 @@ When using [WebSockets](../concepts/managers.md#data-stream) or other real-time high-frequency updates can overwhelm the DevTools extension. Use the `predicate` option to filter out specific action types or schemas: -```tsx title="index.tsx" +```tsx title="index.ts" framework-imports import { getDefaultManagers, actionTypes } from '@data-client/react'; import { Ticker } from './resources/Ticker'; diff --git a/docs/core/getting-started/resource.md b/docs/core/getting-started/resource.md index 44ccb329fcbd..723031c08df6 100644 --- a/docs/core/getting-started/resource.md +++ b/docs/core/getting-started/resource.md @@ -253,8 +253,11 @@ export const TodoResource = { ```ts -import type { Manager, Middleware } from '@data-client/core'; -import type { EndpointInterface } from '@data-client/endpoint'; +import type { + Manager, + Middleware, + EndpointInterface, +} from '@data-client/react'; export default class StreamManager implements Manager { protected declare middleware: Middleware; From 1dcdc85a1841f35251c5f68ce8334bc4df59977b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 04:09:56 +0000 Subject: [PATCH 5/5] docs(skills): Manager skill imports from react/vue, not core Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01XbMoSgP3nZE1bn2HkHpXva --- .agents/skills/data-client-manager/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agents/skills/data-client-manager/SKILL.md b/.agents/skills/data-client-manager/SKILL.md index 69d0dc36e490..c00fd0cf8a98 100644 --- a/.agents/skills/data-client-manager/SKILL.md +++ b/.agents/skills/data-client-manager/SKILL.md @@ -54,7 +54,7 @@ ctrl.fetch(), ctrl.fetchIfStale(), ctrl.expireAll(), ctrl.invalidate(), ctrl.inv ctrl.setError(), ctrl.resetEntireStore(), ctrl.subscribe(), ctrl.unsubscribe(). ```ts -import type { Manager, Middleware } from '@data-client/core'; +import type { Manager, Middleware } from '@data-client/react'; // or '@data-client/vue' import CurrentTime from './CurrentTime'; export default class TimeManager implements Manager {