Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Actions are minimal descriptions of store updates.

They are [dispatched by Controller methods](./Controller.vue.md#action-dispatchers) ->
[read and consumed by Manager middleware](./Manager.vue.md#reading-and-consuming-actions) ->
processed by [reducers](https://react.dev/reference/react/useReducer) registered with [DataClientPlugin](https://dataclient.io/vue/getting-started/installation)
processed by [reducers](https://react.dev/reference/react/useReducer) registered with [DataClientPlugin](https://dataclient.io/vue/api/DataClientPlugin)
to update the store's state.

Many actions use the same meta information:
Expand Down
4 changes: 2 additions & 2 deletions .agents/skills/data-client-manager/references/Manager.vue.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ interface Manager {
The only differences is that the `next()` function returns a `Promise`.

This promise resolves when the reducer update is committed to the
[DataClientPlugin](https://dataclient.io/vue/getting-started/installation) store. This enables building managers that perform work with the
[DataClientPlugin](https://dataclient.io/vue/api/DataClientPlugin) store. This enables building managers that perform work with the
newly computed state.

Since redux is fully synchronous, an adapter must be placed in front of Reactive Data Client style middleware to
Expand All @@ -53,7 +53,7 @@ Provides any cleanup of dangling resources after manager is no longer in use.

## Adding managers to Reactive Data Client {#adding}

Use the `managers` option of [DataClientPlugin](https://dataclient.io/vue/getting-started/installation). The plugin is
Use the [managers](https://dataclient.io/vue/api/DataClientPlugin#managers) option of [DataClientPlugin](https://dataclient.io/vue/api/DataClientPlugin). The plugin is
installed once per app, so managers are created once.

```ts title="main.ts"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# getDefaultManagers()

`getDefaultManagers` returns an Array of [Managers](./Manager.vue.md) to be sent to [DataClientPlugin](https://dataclient.io/vue/getting-started/installation#add-provider-at-top-level-component).
`getDefaultManagers` returns an Array of [Managers](./Manager.vue.md) to be sent to [DataClientPlugin](https://dataclient.io/vue/api/DataClientPlugin).

This makes it simple to configure and add custom [Managers](./Manager.vue.md), while remaining robust against
any potential changes to the default managers.
Expand All @@ -29,8 +29,8 @@ app.mount('#app');
```

When `managers` is omitted, `DataClientPlugin` uses `getDefaultManagers()` with no arguments.
See [installation](https://dataclient.io/vue/getting-started/installation#add-provider-at-top-level-component) for the
other `DataClientPlugin` options.
See [DataClientPlugin](https://dataclient.io/vue/api/DataClientPlugin#options) for the
other options.

## Arguments

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ 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
[DataClientPlugin's `initialState` option](https://dataclient.io/vue/getting-started/installation#add-provider-at-top-level-component). IndexedDB writes are
[DataClientPlugin's `initialState` option](https://dataclient.io/vue/api/DataClientPlugin#initialState). 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](https://dataclient.io/vue/concepts/expiry-policy)
Expand Down
33 changes: 32 additions & 1 deletion .agents/skills/data-client-react/references/DataProvider.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ interface ProviderProps {
managers?: Manager[];
initialState?: State<unknown>;
Controller?: typeof Controller;
gcPolicy?: GCInterface;
devButton?:
| 'bottom-right'
| 'bottom-left'
Expand All @@ -114,9 +115,10 @@ export interface State<T> {
readonly meta: {
readonly [key: string]: {
readonly date: number;
readonly error?: ErrorTypes;
readonly fetchedAt: number;
readonly expiresAt: number;
readonly prevExpiresAt?: number;
readonly error?: ErrorTypes;
readonly invalidated?: boolean;
readonly errorPolicy?: 'hard' | 'soft' | undefined;
};
Expand Down Expand Up @@ -179,6 +181,35 @@ const RealApp = (
);
```

### gcPolicy?: GCInterface {#gcPolicy}

Removes data from the store once no component uses it and it has gone stale. Defaults to
`new GCPolicy()`.

```tsx
import { DataProvider, GCPolicy } from '@data-client/react';

const gcPolicy = new GCPolicy({ intervalMS: 60 * 1000 * 10 });

const RealApp = (
<DataProvider gcPolicy={gcPolicy}>
<App />
</DataProvider>
);
```

```ts title="GCPolicy options"
new GCPolicy({
// how often to sweep (default 5 minutes)
intervalMS: 60 * 1000 * 5,
// how many stale lifetimes before data is removed (default 2)
expiryMultiplier: 2,
// or choose when unused data is removed (replaces expiryMultiplier)
// here: one minute after it goes stale
expiresAt: ({ expiresAt }) => expiresAt + 60 * 1000,
});
```

### devButton

In development, a small button will appear that gives easy access to [browser devtools](https://dataclient.io/docs/getting-started/debugging) if
Expand Down
1 change: 1 addition & 0 deletions .agents/skills/data-client-setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,5 +274,6 @@ Vue projects: read `<name>.vue.md` instead of `<name>.md` when it exists.
For detailed API documentation, see the [references](references/) directory:

- [DataProvider](references/DataProvider.md) - React root provider component (Vue installs `DataClientPlugin` instead; see installation)
- [DataClientPlugin](references/DataClientPlugin.md) - Plugin options (Vue)
- [installation](references/installation.md) - Installation guide
- [getDefaultManagers](references/getDefaultManagers.md) - Default managers
1 change: 1 addition & 0 deletions .agents/skills/data-client-setup/references.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"vue"
],
"docs": {
"DataClientPlugin.md": "docs/core/api/DataClientPlugin.vue.md",
"DataProvider.md": "docs/core/api/DataProvider.md",
"getDefaultManagers.md": "docs/core/api/getDefaultManagers.md",
"installation.md": "docs/core/getting-started/installation.md"
Expand Down
190 changes: 190 additions & 0 deletions .agents/skills/data-client-setup/references/DataClientPlugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
<!-- Generated by `yarn build:skills` from docs/core/api/DataClientPlugin.vue.md (vue). Edit the source doc, not this file. -->

# DataClientPlugin

[Vue plugin](https://vuejs.org/guide/reusability/plugins.html) that creates the store and
[Controller](https://dataclient.io/vue/api/Controller), and provides them to every component in the app. Install it once,
before `app.mount()`; composables only work in components of an app it is installed on.

```ts title="main.ts"
import { createApp } from 'vue';
import { DataClientPlugin } from '@data-client/vue';
import App from './App.vue';

const app = createApp(App);
app.use(DataClientPlugin);
app.mount('#app');
```

[Managers](https://dataclient.io/vue/api/Manager) start when the plugin is installed, and stop when the app is unmounted.

## Options

```ts
app.use(DataClientPlugin, options);
```

```typescript
interface ProvideOptions {
managers?: Manager[];
initialState?: State<unknown>;
Controller?: typeof Controller;
gcPolicy?: GCInterface;
}
```

### managers?: Manager\[] {#managers}

List of [Managers](https://dataclient.io/vue/api/Manager) to use. This is the main extensibility point of the store.

Defaults to [getDefaultManagers()](./getDefaultManagers.vue.md), which can also be used to extend the defaults.

```ts title="main.ts"
import { DataClientPlugin, getDefaultManagers } from '@data-client/vue';

app.use(DataClientPlugin, {
managers: [...getDefaultManagers(), new MyManager()],
});
```

Default Development:

```typescript
[
new DevToolsManager(),
new NetworkManager(),
new SubscriptionManager(PollingSubscription),
];
```

### initialState?: State\<unknown> {#initialState}

Instead of starting with an empty cache, you can provide your own initial state. This can
be useful for testing, or rehydrating the cache state when using server side rendering.
[mockInitialState()](https://dataclient.io/vue/api/mockInitialState) builds one from fixtures.

```ts title="main.ts"
app.use(DataClientPlugin, { initialState: window.__INITIAL_STATE__ });
```

```typescript
export interface State<T> {
readonly entities: {
readonly [entityKey: string]: { readonly [pk: string]: T } | undefined;
};
readonly endpoints: {
readonly [key: string]: unknown | PK[] | PK | undefined;
};
readonly indexes: NormalizedIndex;
readonly meta: {
readonly [key: string]: {
readonly date: number;
readonly fetchedAt: number;
readonly expiresAt: number;
readonly prevExpiresAt?: number;
readonly error?: ErrorTypes;
readonly invalidated?: boolean;
readonly errorPolicy?: 'hard' | 'soft' | undefined;
};
};
readonly entitiesMeta: {
readonly [entityKey: string]: {
readonly [pk: string]: {
readonly date: number;
readonly expiresAt: number;
readonly fetchedAt: number;
};
};
};
readonly optimistic: (SetResponseAction | OptimisticAction)[];
readonly lastReset: number;
}
```

### Controller?: typeof Controller {#Controller}

This allows you to extend [Controller](https://dataclient.io/vue/api/Controller) to provide additional functionality.
This might be useful if you have additional actions you want to dispatch to custom [Managers](https://dataclient.io/vue/api/Manager).

```ts title="main.ts"
import { Controller, DataClientPlugin } from '@data-client/vue';

class MyController extends Controller {
doSomething = () => {
console.log('hi');
};
}

app.use(DataClientPlugin, { Controller: MyController });
```

[useController()](https://dataclient.io/vue/api/useController) and `$dataClient` then return a `MyController` instance;
cast to use its additional members.

### gcPolicy?: GCInterface {#gcPolicy}

Removes data from the store once no component uses it and it has gone stale. By default, nothing
is ever removed.

```ts title="main.ts"
import { DataClientPlugin, GCPolicy } from '@data-client/vue';

app.use(DataClientPlugin, {
// sweep every 10 minutes
gcPolicy: new GCPolicy({ intervalMS: 60 * 1000 * 10 }),
});
```

```ts title="GCPolicy options"
new GCPolicy({
// how often to sweep (default 5 minutes)
intervalMS: 60 * 1000 * 5,
// how many stale lifetimes before data is removed (default 2)
expiryMultiplier: 2,
// or choose when unused data is removed (replaces expiryMultiplier)
// here: one minute after it goes stale
expiresAt: ({ expiresAt }) => expiresAt + 60 * 1000,
});
```

## $dataClient {#dataclient}

The plugin also adds the [Controller](https://dataclient.io/vue/api/Controller) as the `$dataClient` global property, so
templates and Options API components can use it without [useController()](https://dataclient.io/vue/api/useController).

```html title="DeleteTodo.vue"
<script setup lang="ts">
import { TodoResource } from '@/resources/Todo';

defineProps<{ id: number }>();
</script>

<template>
<button @click="$dataClient.fetch(TodoResource.delete, { id })">
Delete
</button>
</template>
```

## Using composables

Composables like [useSuspense()](https://dataclient.io/vue/api/useSuspense) must run during a component's `setup`, so Vue
knows which app's store to use. Awaiting them requires `<script setup>`: in a hand-written
`async setup()`, composables called after the first `await` lose the component instance and throw.

```html title="TodoDetail.vue"
<script setup lang="ts">
import { useSuspense } from '@data-client/vue';
import { TodoResource } from '@/resources/Todo';
import { UserResource } from '@/resources/User';

const todo = await useSuspense(TodoResource.get, { id: 1 });
// still works after the await
const user = await useSuspense(UserResource.get, {
id: todo.value.userId,
});
</script>
```

Components that `await` must render inside a [`<Suspense>`](https://vuejs.org/guide/built-ins/suspense.html)
boundary.
33 changes: 32 additions & 1 deletion .agents/skills/data-client-setup/references/DataProvider.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ interface ProviderProps {
managers?: Manager[];
initialState?: State<unknown>;
Controller?: typeof Controller;
gcPolicy?: GCInterface;
devButton?:
| 'bottom-right'
| 'bottom-left'
Expand All @@ -114,9 +115,10 @@ export interface State<T> {
readonly meta: {
readonly [key: string]: {
readonly date: number;
readonly error?: ErrorTypes;
readonly fetchedAt: number;
readonly expiresAt: number;
readonly prevExpiresAt?: number;
readonly error?: ErrorTypes;
readonly invalidated?: boolean;
readonly errorPolicy?: 'hard' | 'soft' | undefined;
};
Expand Down Expand Up @@ -179,6 +181,35 @@ const RealApp = (
);
```

### gcPolicy?: GCInterface {#gcPolicy}

Removes data from the store once no component uses it and it has gone stale. Defaults to
`new GCPolicy()`.

```tsx
import { DataProvider, GCPolicy } from '@data-client/react';

const gcPolicy = new GCPolicy({ intervalMS: 60 * 1000 * 10 });

const RealApp = (
<DataProvider gcPolicy={gcPolicy}>
<App />
</DataProvider>
);
```

```ts title="GCPolicy options"
new GCPolicy({
// how often to sweep (default 5 minutes)
intervalMS: 60 * 1000 * 5,
// how many stale lifetimes before data is removed (default 2)
expiryMultiplier: 2,
// or choose when unused data is removed (replaces expiryMultiplier)
// here: one minute after it goes stale
expiresAt: ({ expiresAt }) => expiresAt + 60 * 1000,
});
```

### devButton

In development, a small button will appear that gives easy access to [browser devtools](https://dataclient.io/docs/getting-started/debugging) if
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# getDefaultManagers()

`getDefaultManagers` returns an Array of [Managers](https://dataclient.io/vue/api/Manager) to be sent to [DataClientPlugin](./installation.vue.md#add-provider-at-top-level-component).
`getDefaultManagers` returns an Array of [Managers](https://dataclient.io/vue/api/Manager) to be sent to [DataClientPlugin](./DataClientPlugin.md).

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.
Expand All @@ -29,8 +29,8 @@ app.mount('#app');
```

When `managers` is omitted, `DataClientPlugin` uses `getDefaultManagers()` with no arguments.
See [installation](./installation.vue.md#add-provider-at-top-level-component) for the
other `DataClientPlugin` options.
See [DataClientPlugin](./DataClientPlugin.md#options) for the
other options.

## Arguments

Expand Down
Loading
Loading