@nkzw/fate 0.1.2 → 1.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +810 -129
- package/docs/api/classes/FateClient.md +832 -0
- package/docs/api/functions/clientRoot.md +27 -0
- package/docs/api/functions/createClient.md +21 -0
- package/docs/api/functions/createHTTPTransport.md +51 -0
- package/docs/api/functions/createTRPCTransport.md +46 -0
- package/docs/api/functions/getListEntries.md +15 -0
- package/docs/api/functions/getSelectionPlan.md +36 -0
- package/docs/api/functions/isRecord.md +15 -0
- package/docs/api/functions/isViewTag.md +17 -0
- package/docs/api/functions/liveConnectionTopic.md +19 -0
- package/docs/api/functions/liveEntityTopic.md +19 -0
- package/docs/api/functions/liveGlobalConnectionTopic.md +15 -0
- package/docs/api/functions/mutation.md +32 -0
- package/docs/api/functions/toEntityId.md +21 -0
- package/docs/api/functions/view.md +26 -0
- package/docs/api/index.md +81 -0
- package/docs/api/interfaces/FateThenable.md +31 -0
- package/docs/api/interfaces/Transport.md +199 -0
- package/docs/api/server/classes/FateRequestError.md +67 -0
- package/docs/api/server/drizzle/functions/createDrizzleFate.md +431 -0
- package/docs/api/server/drizzle/functions/createDrizzleSourceAdapter.md +21 -0
- package/docs/api/server/drizzle/functions/createDrizzleSourceRegistry.md +21 -0
- package/docs/api/server/drizzle/index.md +23 -0
- package/docs/api/server/drizzle/type-aliases/DrizzleManyToManyConfig.md +15 -0
- package/docs/api/server/drizzle/type-aliases/DrizzleManyToManyInput.md +5 -0
- package/docs/api/server/drizzle/type-aliases/DrizzleQueryExtra.md +21 -0
- package/docs/api/server/drizzle/type-aliases/DrizzleSourceAdapter.md +337 -0
- package/docs/api/server/drizzle/type-aliases/DrizzleViewConfig.md +29 -0
- package/docs/api/server/functions/bindSourceProcedures.md +32 -0
- package/docs/api/server/functions/computed.md +43 -0
- package/docs/api/server/functions/count.md +21 -0
- package/docs/api/server/functions/createFateFetchHandler.md +21 -0
- package/docs/api/server/functions/createFateServer.md +41 -0
- package/docs/api/server/functions/createHonoFateHandler.md +21 -0
- package/docs/api/server/functions/createLiveEventBus.md +17 -0
- package/docs/api/server/functions/createNestedSourcePlan.md +103 -0
- package/docs/api/server/functions/createResolver.md +74 -0
- package/docs/api/server/functions/createSourcePlan.md +95 -0
- package/docs/api/server/functions/createSourceProcedures.md +46 -0
- package/docs/api/server/functions/dataView.md +32 -0
- package/docs/api/server/functions/field.md +15 -0
- package/docs/api/server/functions/getNestedSelection.md +19 -0
- package/docs/api/server/functions/getScopedArgs.md +21 -0
- package/docs/api/server/functions/hasNestedSelection.md +19 -0
- package/docs/api/server/functions/list.md +28 -0
- package/docs/api/server/functions/refetchSourceById.md +55 -0
- package/docs/api/server/functions/resolveSourceById.md +55 -0
- package/docs/api/server/functions/resolveSourceByIds.md +55 -0
- package/docs/api/server/functions/resolveSourceConnection.md +67 -0
- package/docs/api/server/functions/resolver.md +42 -0
- package/docs/api/server/functions/toPrismaSelect.md +25 -0
- package/docs/api/server/functions/withConnection.md +24 -0
- package/docs/api/server/index.md +109 -0
- package/docs/api/server/prisma/functions/createPrismaFate.md +435 -0
- package/docs/api/server/prisma/functions/createPrismaSourceAdapter.md +21 -0
- package/docs/api/server/prisma/functions/createPrismaSourceRegistry.md +21 -0
- package/docs/api/server/prisma/functions/prismaConnectionArgs.md +57 -0
- package/docs/api/server/prisma/index.md +23 -0
- package/docs/api/server/prisma/type-aliases/PrismaDelegate.md +59 -0
- package/docs/api/server/prisma/type-aliases/PrismaQueryExtra.md +5 -0
- package/docs/api/server/prisma/type-aliases/PrismaSourceAdapter.md +343 -0
- package/docs/api/server/prisma/type-aliases/PrismaViewConfig.md +31 -0
- package/docs/api/server/type-aliases/ComputedField.md +55 -0
- package/docs/api/server/type-aliases/ComputedSelection.md +5 -0
- package/docs/api/server/type-aliases/ConnectionItem.md +29 -0
- package/docs/api/server/type-aliases/ConnectionPagination.md +39 -0
- package/docs/api/server/type-aliases/ConnectionResult.md +29 -0
- package/docs/api/server/type-aliases/CountSelection.md +29 -0
- package/docs/api/server/type-aliases/CountWhere.md +5 -0
- package/docs/api/server/type-aliases/DataViewListOptions.md +13 -0
- package/docs/api/server/type-aliases/DataViewOrderBy.md +5 -0
- package/docs/api/server/type-aliases/DataViewOrderDirection.md +5 -0
- package/docs/api/server/type-aliases/DataViewResult.md +11 -0
- package/docs/api/server/type-aliases/Entity.md +21 -0
- package/docs/api/server/type-aliases/FateServer.md +75 -0
- package/docs/api/server/type-aliases/FateServerManifest.md +5 -0
- package/docs/api/server/type-aliases/FieldSelection.md +21 -0
- package/docs/api/server/type-aliases/LiveConnectionEventType.md +5 -0
- package/docs/api/server/type-aliases/LiveConnectionSourceEvent.md +5 -0
- package/docs/api/server/type-aliases/LiveEventBus.md +5 -0
- package/docs/api/server/type-aliases/LiveEventType.md +5 -0
- package/docs/api/server/type-aliases/LiveSourceEvent.md +5 -0
- package/docs/api/server/type-aliases/NativeFateAPI.md +47 -0
- package/docs/api/server/type-aliases/OrderDirection.md +5 -0
- package/docs/api/server/type-aliases/SourceConfig.md +55 -0
- package/docs/api/server/type-aliases/SourceDefinition.md +55 -0
- package/docs/api/server/type-aliases/SourceOrder.md +5 -0
- package/docs/api/server/type-aliases/SourceOrderField.md +21 -0
- package/docs/api/server/type-aliases/SourcePlan.md +29 -0
- package/docs/api/server/type-aliases/SourcePlanNode.md +29 -0
- package/docs/api/server/type-aliases/SourceRegistry.md +11 -0
- package/docs/api/server/type-aliases/SourceRelation.md +15 -0
- package/docs/api/server/type-aliases/SourceRelationConfig.md +23 -0
- package/docs/api/server/variables/byIdInput.md +5 -0
- package/docs/api/server/variables/connectionArgs.md +7 -0
- package/docs/api/type-aliases/ConnectionMetadata.md +7 -0
- package/docs/api/type-aliases/ConnectionRef.md +13 -0
- package/docs/api/type-aliases/Entity.md +15 -0
- package/docs/api/type-aliases/EntityId.md +7 -0
- package/docs/api/type-aliases/FateLiveConnectionEvent.md +5 -0
- package/docs/api/type-aliases/FateLiveEvent.md +5 -0
- package/docs/api/type-aliases/FateMutations.md +7 -0
- package/docs/api/type-aliases/FateOperation.md +5 -0
- package/docs/api/type-aliases/FateProtocolRequest.md +5 -0
- package/docs/api/type-aliases/FateProtocolResponse.md +5 -0
- package/docs/api/type-aliases/FateRecord.md +7 -0
- package/docs/api/type-aliases/FateRoots.md +7 -0
- package/docs/api/type-aliases/InferFateAPI.md +11 -0
- package/docs/api/type-aliases/List.md +5 -0
- package/docs/api/type-aliases/ListEntry.md +5 -0
- package/docs/api/type-aliases/ListItem.md +13 -0
- package/docs/api/type-aliases/Mask.md +15 -0
- package/docs/api/type-aliases/MutationDefinition.md +21 -0
- package/docs/api/type-aliases/MutationEntity.md +13 -0
- package/docs/api/type-aliases/MutationIdentifier.md +19 -0
- package/docs/api/type-aliases/MutationInput.md +13 -0
- package/docs/api/type-aliases/MutationResult.md +13 -0
- package/docs/api/type-aliases/NodesItem.md +13 -0
- package/docs/api/type-aliases/Pagination.md +39 -0
- package/docs/api/type-aliases/Request.md +7 -0
- package/docs/api/type-aliases/RequestMode.md +7 -0
- package/docs/api/type-aliases/RequestOptions.md +7 -0
- package/docs/api/type-aliases/RequestResult.md +18 -0
- package/docs/api/type-aliases/Selection.md +13 -0
- package/docs/api/type-aliases/Snapshot.md +7 -0
- package/docs/api/type-aliases/TypeConfig.md +41 -0
- package/docs/api/type-aliases/View.md +17 -0
- package/docs/api/type-aliases/ViewData.md +18 -0
- package/docs/api/type-aliases/ViewEntity.md +13 -0
- package/docs/api/type-aliases/ViewEntityName.md +13 -0
- package/docs/api/type-aliases/ViewRef.md +13 -0
- package/docs/api/type-aliases/ViewSelection.md +13 -0
- package/docs/api/type-aliases/ViewSnapshot.md +18 -0
- package/docs/api/type-aliases/ViewTag.md +7 -0
- package/docs/api/variables/ConnectionTag.md +7 -0
- package/docs/guide/actions.md +263 -0
- package/docs/guide/core-concepts.md +22 -0
- package/docs/guide/getting-started.md +57 -0
- package/docs/guide/list-views.md +86 -0
- package/docs/guide/live-views.md +245 -0
- package/docs/guide/requests.md +130 -0
- package/docs/guide/server-integration.md +630 -0
- package/docs/guide/views.md +278 -0
- package/docs/guide/void-integration.md +167 -0
- package/docs/index.md +4 -0
- package/lib/cli.mjs +17 -250
- package/lib/clientStub.d.mts +6 -0
- package/lib/clientStub.mjs +6 -0
- package/lib/executor-BqwHdN3n.d.mts +468 -0
- package/lib/index.d.mts +64 -3
- package/lib/index.mjs +1965 -559
- package/lib/list-4wRNSVgI.d.mts +10 -0
- package/lib/list-C_mi6GnE.mjs +16 -0
- package/lib/list.d.mts +2 -0
- package/lib/list.mjs +2 -0
- package/lib/liveTopics-DzNtJaBD.mjs +197 -0
- package/lib/{record-DnhZuvUe.mjs → record-AeZJC9fd.mjs} +1 -2
- package/lib/record-ENh92gyb.d.mts +315 -0
- package/lib/server/drizzle.d.mts +304 -0
- package/lib/server/drizzle.mjs +688 -0
- package/lib/server/prisma.d.mts +309 -0
- package/lib/server/prisma.mjs +345 -0
- package/lib/server.d.mts +82 -184
- package/lib/server.mjs +870 -416
- package/lib/sourceRouter-Bwpkr062.mjs +972 -0
- package/lib/{types-Djlh7mMH.d.mts → types-Dz46PXr3.d.mts} +327 -197
- package/lib/vite.d.mts +17 -0
- package/lib/vite.mjs +592 -0
- package/package.json +44 -6
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Core Concepts
|
|
2
|
+
|
|
3
|
+
**_fate_** has a minimal API surface and is aimed at reducing data fetching complexity.
|
|
4
|
+
|
|
5
|
+
## Thinking in Views
|
|
6
|
+
|
|
7
|
+
In fate, each component declares the data it needs using views. Views are composed upward through the component tree until they reach a root, where the actual request is made. fate fetches all required data in a single request. React Suspense manages loading states, and any data-fetching errors naturally bubble up to React error boundaries. This eliminates the need for imperative loading logic or manual error handling.
|
|
8
|
+
|
|
9
|
+
Traditionally, React apps are built with components and hooks. fate introduces a third primitive: views – a declarative way for components to express their data requirements. An app built with fate looks more like this:
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<picture class="fate-tree">
|
|
13
|
+
<source media="(prefers-color-scheme: dark)" srcset="/public/fate-tree-dark.svg">
|
|
14
|
+
<source media="(prefers-color-scheme: light)" srcset="/public/fate-tree.svg">
|
|
15
|
+
<img alt="Tree" src="/public/fate-tree.svg" width="90%">
|
|
16
|
+
</picture>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
With fate, you no longer worry about _when_ to fetch data, how to coordinate loading states, or how to handle errors imperatively. You avoid overfetching, stop passing unnecessary data down the tree, and eliminate boilerplate types created solely for passing server data to child components.
|
|
20
|
+
|
|
21
|
+
> [!NOTE]
|
|
22
|
+
> Views in _fate_ are what fragments are in GraphQL.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
## Template
|
|
4
|
+
|
|
5
|
+
Create a new fate app with Vite+:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
vp create fate my-app
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The template selector can create a Void app with Drizzle, a tRPC app with Drizzle, or a tRPC app with Prisma. The template sources live in the fate repo under [`packages/create-fate/templates/fate`](https://github.com/nkzw-tech/fate/tree/main/packages/create-fate/templates/fate). They feature modern tools to deliver an incredibly fast development experience.
|
|
12
|
+
|
|
13
|
+
## Manual Installation
|
|
14
|
+
|
|
15
|
+
**_fate_** requires React 19.2+. For a React client, install `react-fate`:
|
|
16
|
+
|
|
17
|
+
::: code-group
|
|
18
|
+
|
|
19
|
+
```bash [npm]
|
|
20
|
+
npm add react-fate
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```bash [pnpm]
|
|
24
|
+
pnpm add react-fate
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```bash [yarn]
|
|
28
|
+
yarn add react-fate
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
:::
|
|
32
|
+
|
|
33
|
+
If your server is a separate package, install `@nkzw/fate` there as a runtime dependency too. Install `@nkzw/fate` on the client only for a barebones integration without React:
|
|
34
|
+
|
|
35
|
+
::: code-group
|
|
36
|
+
|
|
37
|
+
```bash [npm]
|
|
38
|
+
npm add @nkzw/fate
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```bash [pnpm]
|
|
42
|
+
pnpm add @nkzw/fate
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```bash [yarn]
|
|
46
|
+
yarn add @nkzw/fate
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
:::
|
|
50
|
+
|
|
51
|
+
> [!WARNING]
|
|
52
|
+
>
|
|
53
|
+
> **_fate_** is currently in alpha and not production ready. If something doesn't work for you, please open a pull request.
|
|
54
|
+
|
|
55
|
+
If you'd like to try the example app in GitHub Codespaces, click the button below:
|
|
56
|
+
|
|
57
|
+
[](https://github.com/codespaces/new?repo=nkzw-tech/fate)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# List Views
|
|
2
|
+
|
|
3
|
+
## Pagination with `useListView`
|
|
4
|
+
|
|
5
|
+
You can wrap a list of references using `useListView` to enable connection-style lists with pagination support.
|
|
6
|
+
|
|
7
|
+
For example, you can define a `CommentView` and reuse it inside of a `CommentConnectionView`:
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { useListView, ViewRef } from 'react-fate';
|
|
11
|
+
|
|
12
|
+
const CommentView = view<Comment>()({
|
|
13
|
+
content: true,
|
|
14
|
+
id: true,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const CommentConnectionView = {
|
|
18
|
+
args: { first: 10 },
|
|
19
|
+
items: {
|
|
20
|
+
node: CommentView,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const PostView = view<Post>()({
|
|
25
|
+
comments: CommentConnectionView,
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Now you can apply the `useListView` hook inside of your `PostCard` component to read the list of comments and load more comments when needed:
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
export function PostCard({ detail, post: postRef }: { detail?: boolean; post: ViewRef<'Post'> }) {
|
|
33
|
+
const post = useView(PostView, postRef);
|
|
34
|
+
const [comments, loadNext] = useListView(CommentConnectionView, post.comments);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div>
|
|
38
|
+
{comments.map(({ node }) => (
|
|
39
|
+
<CommentCard comment={node} key={node.id} post={post} />
|
|
40
|
+
))}
|
|
41
|
+
{loadNext ? (
|
|
42
|
+
<Button onClick={loadNext} variant="ghost">
|
|
43
|
+
Load more comments
|
|
44
|
+
</Button>
|
|
45
|
+
) : null}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
If `loadNext` is undefined, it means there are no more comments to load. If you want to instead load previous comments, you can use the third argument returned by `useListView`, which is `loadPrevious`. Similarly, if there are no previous comments to load, `loadPrevious` will be undefined.
|
|
52
|
+
|
|
53
|
+
## Pagination Arguments
|
|
54
|
+
|
|
55
|
+
Connection views can define default arguments, and `useListView` carries those arguments forward when loading more pages:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
const CommentConnectionView = {
|
|
59
|
+
args: { first: 10 },
|
|
60
|
+
items: {
|
|
61
|
+
cursor: true,
|
|
62
|
+
node: CommentView,
|
|
63
|
+
},
|
|
64
|
+
pagination: {
|
|
65
|
+
hasNext: true,
|
|
66
|
+
hasPrevious: true,
|
|
67
|
+
nextCursor: true,
|
|
68
|
+
previousCursor: true,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
When `loadNext` runs, fate sends the next cursor as `after` and keeps the page size in `first`. When `loadPrevious` runs, fate sends the previous cursor as `before` and uses `last` for the page size. This lets the server distinguish forward and backward pagination while keeping the component API small.
|
|
74
|
+
|
|
75
|
+
Additional arguments on a root request are scoped to that root list:
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
const { posts } = useRequest({
|
|
79
|
+
posts: {
|
|
80
|
+
args: { categoryId: category.id, first: 20 },
|
|
81
|
+
list: PostConnectionView,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The `categoryId` list above has its own cache entry and pagination state. Loading another page for that list does not update a different `posts` request with another category or search query.
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Live Views
|
|
2
|
+
|
|
3
|
+
`useLiveView` resolves a `ViewRef` just like `useView`, but also keeps the selected object up to date through the native live SSE transport.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { useLiveView, ViewRef } from 'react-fate';
|
|
7
|
+
|
|
8
|
+
export const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
|
|
9
|
+
const post = useLiveView(PostView, postRef);
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<Card>
|
|
13
|
+
<h2>{post.title}</h2>
|
|
14
|
+
{/* Updates automatically! */}
|
|
15
|
+
<p>{post.likes} likes</p>
|
|
16
|
+
</Card>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The API mirrors `useView`: pass a view and a ref, and get back the same masked data shape. A `null` ref returns `null` and does not subscribe.
|
|
22
|
+
|
|
23
|
+
## How Live Updates Work
|
|
24
|
+
|
|
25
|
+
The native HTTP transport opens one Server-Sent Events (SSE) connection per Fate client. When components mount or unmount live views, the client sends subscribe and unsubscribe control messages to the server. The server keeps those selections on the connection and sends updates only for records that connection subscribed to.
|
|
26
|
+
|
|
27
|
+
When the server sends an update, fate normalizes the selected record into the same cache used by requests, actions, and mutations. Components that read affected fields re-render automatically.
|
|
28
|
+
|
|
29
|
+
For example, if `PostView` selects `likes`, a live update that changes `likes` re-renders the `PostCard`. If another component only selected `title`, it does not re-render for a `likes` change.
|
|
30
|
+
|
|
31
|
+
Live deletion events remove the record from the normalized cache in the same way as mutations, and any lists or object fields that reference it are pruned.
|
|
32
|
+
|
|
33
|
+
## Client Setup
|
|
34
|
+
|
|
35
|
+
Generate the client with the native transport and point it at your Fate endpoint:
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { FateClient } from 'react-fate';
|
|
39
|
+
import { createFateClient } from 'react-fate/client';
|
|
40
|
+
|
|
41
|
+
export function App() {
|
|
42
|
+
const fate = useMemo(
|
|
43
|
+
() =>
|
|
44
|
+
createFateClient({
|
|
45
|
+
fetch: (input, init) =>
|
|
46
|
+
fetch(input, {
|
|
47
|
+
...init,
|
|
48
|
+
credentials: 'include',
|
|
49
|
+
}),
|
|
50
|
+
url: `${env('SERVER_URL')}/fate`,
|
|
51
|
+
}),
|
|
52
|
+
[],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return <FateClient client={fate}>{/* Components go here */}</FateClient>;
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
> [!NOTE]
|
|
60
|
+
>
|
|
61
|
+
> Live views use `GET /fate/live` for the single SSE stream and `POST /fate/live` for subscribe/unsubscribe control messages.
|
|
62
|
+
|
|
63
|
+
## Server Setup
|
|
64
|
+
|
|
65
|
+
Live views use an event bus. By default, the bus signals that an object changed and fate refetches the selected object through the same data view pipeline used by `byId` queries before sending it to the client. Update events can also include changed field paths so fate only resolves the intersection of those paths and each active subscription.
|
|
66
|
+
|
|
67
|
+
Pass a live event bus to `createFateServer` and expose the native handler:
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { createFateServer, createHonoFateHandler, createLiveEventBus } from '@nkzw/fate/server';
|
|
71
|
+
import type { AppContext } from './context.ts';
|
|
72
|
+
import { sources } from './sources.ts';
|
|
73
|
+
import { Root } from './views.ts';
|
|
74
|
+
|
|
75
|
+
export const live = createLiveEventBus();
|
|
76
|
+
|
|
77
|
+
export const fate = createFateServer<AppContext>({
|
|
78
|
+
live,
|
|
79
|
+
roots: Root,
|
|
80
|
+
sources,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
app.all('/fate/*', createHonoFateHandler(fate));
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Once this is in place, components can switch from `useView` to `useLiveView` without changing their view definitions or return types.
|
|
87
|
+
|
|
88
|
+
## Live List Views
|
|
89
|
+
|
|
90
|
+
`useLiveListView` mirrors `useListView`, but subscribes to live connection events for the connection it receives:
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { useLiveListView, useLiveView, ViewRef } from 'react-fate';
|
|
94
|
+
|
|
95
|
+
export function PostCard({ post: postRef }: { post: ViewRef<'Post'> }) {
|
|
96
|
+
const post = useLiveView(PostView, postRef);
|
|
97
|
+
const [comments, loadNext] = useLiveListView(CommentConnectionView, post.comments);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<>
|
|
101
|
+
{comments.map(({ node }) => (
|
|
102
|
+
<CommentCard comment={node} key={node.id} />
|
|
103
|
+
))}
|
|
104
|
+
{loadNext ? <button onClick={loadNext}>Load more</button> : null}
|
|
105
|
+
</>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The hook returns the same tuple as `useListView`: items, `loadNext`, and `loadPrevious`. Live events append, prepend, insert, or delete edges from one connection without deleting the underlying records.
|
|
111
|
+
|
|
112
|
+
By default, live appends and prepends respect pagination boundaries. If the relevant edge still has more pages, fate keeps the incoming node attached to that edge instead of expanding the loaded window. For chat or activity streams where new items should keep appearing immediately, opt into visible live insertion on the connection view:
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
const MessageConnectionView = {
|
|
116
|
+
args: { first: 30 },
|
|
117
|
+
items: {
|
|
118
|
+
node: MessageView,
|
|
119
|
+
},
|
|
120
|
+
live: {
|
|
121
|
+
append: 'visible',
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Emit connection events on the server when list membership changes:
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
live.connection('Post.comments', { id: postId }).prependNode('Comment', comment.id);
|
|
130
|
+
live.connection('Post.comments', { id: postId }).deleteEdge('Comment', comment.id);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
For root lists, use the generated root procedure name:
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
live.connection('posts', { categoryId }).prependNode('Post', post.id);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
If the changed list cannot be described precisely, invalidate the active connection and fate will refetch it:
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
live.connection('posts', { categoryId }).invalidate();
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Connection identity follows Relay's model: pagination args like `first`, `last`, `after`, and `before` are ignored for live connection matching, while filter args such as `categoryId` are part of the identity.
|
|
146
|
+
|
|
147
|
+
## Emitting Events
|
|
148
|
+
|
|
149
|
+
After a mutation changes an object, emit an update event for that object:
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
export const postRouter = router({
|
|
153
|
+
...fate.procedures({
|
|
154
|
+
view: postDataView,
|
|
155
|
+
}),
|
|
156
|
+
like: procedure.input(likeInput).mutation(async ({ ctx, input }) => {
|
|
157
|
+
const post = await ctx.prisma.post.update({
|
|
158
|
+
data: {
|
|
159
|
+
likes: {
|
|
160
|
+
increment: 1,
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
where: { id: input.id },
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
live.update('Post', input.id);
|
|
167
|
+
|
|
168
|
+
return post;
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
This tells fate that the `Post` changed. Every active live view for that post refreshes using the selection it subscribed with.
|
|
174
|
+
|
|
175
|
+
If you know which fields changed, pass them with `changed` to reduce the amount of data sent to each subscriber:
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
live.update('Post', input.id, { changed: ['likes'] });
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
With this version, a live view that selected `likes` refreshes only `likes`, while a live view that only selected unrelated fields is skipped entirely.
|
|
182
|
+
|
|
183
|
+
If a mutation changes a related object, emit for the object whose live view should refresh. For example, adding a comment usually changes the post's `commentCount` and `comments` list, so emit for the `Post`:
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
export const commentRouter = router({
|
|
187
|
+
add: procedure.input(addCommentInput).mutation(async ({ ctx, input }) => {
|
|
188
|
+
const comment = await ctx.prisma.comment.create({
|
|
189
|
+
data: {
|
|
190
|
+
content: input.content,
|
|
191
|
+
postId: input.postId,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
live.update('Post', input.postId, { changed: ['commentCount', 'comments'] });
|
|
196
|
+
|
|
197
|
+
return comment;
|
|
198
|
+
}),
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
For deletions, emit a delete event for the deleted object if clients may be subscribed to it:
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
live.delete('Comment', input.id);
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
If deleting the object also changes another object, emit an update for that object too:
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
live.update('Post', postId, { changed: ['commentCount', 'comments'] });
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
You can pass an `eventId` when emitting. fate sends it on the native SSE event and includes the last received event ID when it resubscribes after a reconnect:
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
live.update('Post', input.id, {
|
|
218
|
+
changed: ['likes'],
|
|
219
|
+
eventId: `post:${input.id}:${Date.now()}`,
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The default `createLiveEventBus` is an in-memory fanout bus and does not replay events that were emitted while a client was disconnected. Use a durable custom live bus if your deployment needs reconnects to catch up from `lastEventId`; otherwise the client receives future live events after it reconnects.
|
|
224
|
+
|
|
225
|
+
## Error Handling
|
|
226
|
+
|
|
227
|
+
Live subscription errors are reported out of band. They do not replace the last cached data or throw through the component that called `useLiveView`.
|
|
228
|
+
|
|
229
|
+
Pass `onLiveError` when creating the client to send those failures to your logger or monitoring system:
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
const fate = createFateClient({
|
|
233
|
+
fetch: (input, init) =>
|
|
234
|
+
fetch(input, {
|
|
235
|
+
...init,
|
|
236
|
+
credentials: 'include',
|
|
237
|
+
}),
|
|
238
|
+
onLiveError(error) {
|
|
239
|
+
captureException(error);
|
|
240
|
+
},
|
|
241
|
+
url: `${env('SERVER_URL')}/fate`,
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The handler runs in a microtask after the subscription reports the error. Components continue to read whatever data is currently available in the fate cache.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Requests
|
|
2
|
+
|
|
3
|
+
## Requesting Lists
|
|
4
|
+
|
|
5
|
+
The `useRequest` hook can be used to declare our data needs for a specific screen or component tree. At the root of our app, we can request a list of posts like this:
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { useRequest } from 'react-fate';
|
|
9
|
+
import { PostCard, PostView } from './PostCard.tsx';
|
|
10
|
+
|
|
11
|
+
export function App() {
|
|
12
|
+
const { posts } = useRequest({ posts: { list: PostView } });
|
|
13
|
+
return posts.map((post) => <PostCard key={post.id} post={post} />);
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This component suspends or throws errors, which bubble up to the nearest error boundary. Wrap your component tree with `ErrorBoundary` and `Suspense` components to show error and loading states:
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
<ErrorBoundary FallbackComponent={ErrorComponent}>
|
|
21
|
+
<Suspense fallback={<div>Loading…</div>}>
|
|
22
|
+
<App />
|
|
23
|
+
</Suspense>
|
|
24
|
+
</ErrorBoundary>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
> [!NOTE]
|
|
28
|
+
>
|
|
29
|
+
> `useRequest` might issue multiple requests which are automatically batched together by tRPC's [HTTP Batch Link](https://trpc.io/docs/client/links/httpBatchLink) into a single network request.
|
|
30
|
+
|
|
31
|
+
## Requesting Objects by ID
|
|
32
|
+
|
|
33
|
+
If you want to fetch data for a single object instead of a list, you can specify the `id` and the associated `view` like this:
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
const { post } = useRequest({
|
|
37
|
+
post: { id: '12', view: PostView },
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If you want to fetch multiple objects by their IDs, you can use the `ids` field:
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
const { posts } = useRequest({
|
|
45
|
+
posts: { ids: ['6', '7'], view: PostView },
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Other Types of Requests
|
|
50
|
+
|
|
51
|
+
For any other queries, pass only the `type` and `view`:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const { viewer } = useRequest({
|
|
55
|
+
viewer: { view: UserView },
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Request Arguments
|
|
60
|
+
|
|
61
|
+
You can pass arguments to `useRequest` calls. This is useful for pagination, filtering, or sorting. For example, to fetch the first 10 posts, you can do the following:
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
const { posts } = useRequest({
|
|
65
|
+
posts: {
|
|
66
|
+
args: { first: 10 },
|
|
67
|
+
list: PostView,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Request arguments are part of the cache key. Two list requests for the same root with different filters or sorting arguments keep separate list state, and cursor arguments are merged into the same list when you load more pages. The selected view is part of the key as well: requesting `PostCardView` and `PostDetailView` can share normalized records, but fate still tracks whether the specific fields for each request are present.
|
|
73
|
+
|
|
74
|
+
## Request Modes
|
|
75
|
+
|
|
76
|
+
`useRequest` supports different request modes to control caching and data freshness. The available modes are:
|
|
77
|
+
|
|
78
|
+
- `cache-first` (_default_): Returns data from the cache if available, otherwise fetches from the network.
|
|
79
|
+
- `stale-while-revalidate`: Returns data from the cache and simultaneously fetches fresh data from the network.
|
|
80
|
+
- `network-only`: Always fetches data from the network, bypassing the cache.
|
|
81
|
+
|
|
82
|
+
You can pass the request mode as an option to `useRequest`:
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
const { posts } = useRequest(
|
|
86
|
+
{
|
|
87
|
+
posts: { list: PostView },
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
mode: 'stale-while-revalidate',
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Cache Lifetime
|
|
96
|
+
|
|
97
|
+
fate stores records in a normalized cache keyed by `__typename` and `id`. Lists and root queries point at those records, and views read from the normalized cache. When a `useRequest` call is mounted, fate retains the request so the records and lists needed by that screen stay in memory. When the component unmounts, the request is released and fate schedules garbage collection.
|
|
98
|
+
|
|
99
|
+
Released requests are kept in a small release buffer before their data becomes collectible. This makes common route transitions cheap: navigating away from a screen and quickly coming back usually reuses the cached records instead of refetching them. The default release buffer stores the 10 most recently released requests.
|
|
100
|
+
|
|
101
|
+
You can tune the buffer when creating the client:
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
const fate = createClient({
|
|
105
|
+
gcReleaseBufferSize: 20,
|
|
106
|
+
roots,
|
|
107
|
+
transport,
|
|
108
|
+
types,
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Set `gcReleaseBufferSize` to `0` in tests or very memory-sensitive environments when released screens should be collected immediately.
|
|
113
|
+
|
|
114
|
+
`cache-first` request handles are stable while their request is cached. If garbage collection later removes the data for a fulfilled request, the next `cache-first` request automatically fetches it again rather than returning stale references.
|
|
115
|
+
|
|
116
|
+
If you call `fate.request(...)` outside React and need the result to stay in memory across manual `gc()` calls, retain the same request for the lifetime of that work:
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
const request = { posts: { list: PostView } };
|
|
120
|
+
const retained = fate.retain(request);
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const { posts } = await fate.request(request);
|
|
124
|
+
// Use posts while this request is retained.
|
|
125
|
+
} finally {
|
|
126
|
+
retained.dispose();
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Garbage collection waits for active optimistic updates to settle before sweeping records. This keeps temporary optimistic records and their list positions stable while mutations are still pending.
|