@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
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
</picture>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
-
**_fate_** is a modern data client for React
|
|
11
|
+
**_fate_** is a modern data client for React inspired by [Relay](https://relay.dev/) and [GraphQL](https://graphql.org/). It combines view composition, normalized caching, data masking, Async React features, and type-safe data fetching.
|
|
12
12
|
|
|
13
13
|
### Features
|
|
14
14
|
|
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
- **Async React:** fate uses modern Async React features like Actions, Suspense, and `use` to support concurrent rendering and enable a seamless user experience.
|
|
19
19
|
- **Lists & Pagination:** fate provides built-in support for connection-style lists with cursor-based pagination, making it easy to implement infinite scrolling and "load-more" functionality.
|
|
20
20
|
- **Optimistic Updates:** fate supports declarative optimistic updates for mutations, allowing the UI to update immediately while the server request is in-flight. If the request fails, the cache and its associated views are rolled back to their previous state.
|
|
21
|
+
- **Live Views:** fate can keep individual view refs up to date through a single native Server-Sent Events stream, merging updates into the normalized cache.
|
|
21
22
|
- **AI-Ready:** fate's minimal, predictable API and explicit data selection enable local reasoning, enabling humans and AI tools to generate stable, type-safe data-fetching code.
|
|
22
23
|
|
|
23
|
-
### A modern data client for React
|
|
24
|
+
### A modern data client for React
|
|
24
25
|
|
|
25
26
|
**_fate_** is designed to make data fetching and state management in React applications more composable, declarative, and predictable. The framework has a minimal API, no DSL, and no magic—_it's just JavaScript_.
|
|
26
27
|
|
|
@@ -32,7 +33,7 @@ However, GraphQL comes with its own type system and query language. If you are a
|
|
|
32
33
|
|
|
33
34
|
Many React data frameworks lack Relay's ergonomics, especially fragment composition, co-located data requirements, predictable caching, and deep integration with modern React features. Optimistic updates usually require manually managing keys and imperative data updates, which is error-prone and tedious.
|
|
34
35
|
|
|
35
|
-
_fate_ takes the great ideas from Relay and
|
|
36
|
+
_fate_ takes the great ideas from Relay and applies them to plain TypeScript data fetching. You get type safety between the client and server, a native protocol with optional adapters such as tRPC, and GraphQL-like ergonomics for data fetching. Using _fate_ usually looks like this:
|
|
36
37
|
|
|
37
38
|
```tsx
|
|
38
39
|
export const PostView = view<Post>()({
|
|
@@ -55,35 +56,23 @@ export const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
|
|
|
55
56
|
};
|
|
56
57
|
```
|
|
57
58
|
|
|
58
|
-
_[Learn more](/docs/guide/getting-started.md) about fate's core concepts or
|
|
59
|
+
_[Learn more](/docs/guide/getting-started.md) about fate's core concepts or create an app from one of the templates._
|
|
59
60
|
|
|
60
61
|
## Getting Started
|
|
61
62
|
|
|
62
63
|
### Template
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
Create a new fate app with Vite+:
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
```bash [npm]
|
|
69
|
-
npx giget@latest gh:nkzw-tech/fate-template
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
```bash [pnpm]
|
|
73
|
-
pnpx giget@latest gh:nkzw-tech/fate-template
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
```bash [yarn]
|
|
77
|
-
yarn dlx giget@latest gh:nkzw-tech/fate-template
|
|
67
|
+
```bash
|
|
68
|
+
vp create fate my-app
|
|
78
69
|
```
|
|
79
70
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
`fate-template` comes with a simple tRPC backend and a React frontend using **_fate_**. It features modern tools to deliver an incredibly fast development experience. Follow its [README.md](https://github.com/nkzw-tech/fate-template#fate-quick-start-template) to get started.
|
|
71
|
+
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.
|
|
83
72
|
|
|
84
73
|
### Manual Installation
|
|
85
74
|
|
|
86
|
-
**_fate_** requires React 19.2+. For
|
|
75
|
+
**_fate_** requires React 19.2+. For a React client, install `react-fate`:
|
|
87
76
|
|
|
88
77
|
::: code-group
|
|
89
78
|
|
|
@@ -101,7 +90,7 @@ yarn add react-fate
|
|
|
101
90
|
|
|
102
91
|
:::
|
|
103
92
|
|
|
104
|
-
|
|
93
|
+
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:
|
|
105
94
|
|
|
106
95
|
::: code-group
|
|
107
96
|
|
|
@@ -500,6 +489,8 @@ const { posts } = useRequest({
|
|
|
500
489
|
});
|
|
501
490
|
```
|
|
502
491
|
|
|
492
|
+
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.
|
|
493
|
+
|
|
503
494
|
### Request Modes
|
|
504
495
|
|
|
505
496
|
`useRequest` supports different request modes to control caching and data freshness. The available modes are:
|
|
@@ -521,6 +512,43 @@ const { posts } = useRequest(
|
|
|
521
512
|
);
|
|
522
513
|
```
|
|
523
514
|
|
|
515
|
+
### Cache Lifetime
|
|
516
|
+
|
|
517
|
+
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.
|
|
518
|
+
|
|
519
|
+
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.
|
|
520
|
+
|
|
521
|
+
You can tune the buffer when creating the client:
|
|
522
|
+
|
|
523
|
+
```tsx
|
|
524
|
+
const fate = createClient({
|
|
525
|
+
gcReleaseBufferSize: 20,
|
|
526
|
+
roots,
|
|
527
|
+
transport,
|
|
528
|
+
types,
|
|
529
|
+
});
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
Set `gcReleaseBufferSize` to `0` in tests or very memory-sensitive environments when released screens should be collected immediately.
|
|
533
|
+
|
|
534
|
+
`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.
|
|
535
|
+
|
|
536
|
+
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:
|
|
537
|
+
|
|
538
|
+
```tsx
|
|
539
|
+
const request = { posts: { list: PostView } };
|
|
540
|
+
const retained = fate.retain(request);
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
const { posts } = await fate.request(request);
|
|
544
|
+
// Use posts while this request is retained.
|
|
545
|
+
} finally {
|
|
546
|
+
retained.dispose();
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
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.
|
|
551
|
+
|
|
524
552
|
## List Views
|
|
525
553
|
|
|
526
554
|
### Pagination with `useListView`
|
|
@@ -573,6 +601,287 @@ export function PostCard({ detail, post: postRef }: { detail?: boolean; post: Vi
|
|
|
573
601
|
|
|
574
602
|
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.
|
|
575
603
|
|
|
604
|
+
### Pagination Arguments
|
|
605
|
+
|
|
606
|
+
Connection views can define default arguments, and `useListView` carries those arguments forward when loading more pages:
|
|
607
|
+
|
|
608
|
+
```tsx
|
|
609
|
+
const CommentConnectionView = {
|
|
610
|
+
args: { first: 10 },
|
|
611
|
+
items: {
|
|
612
|
+
cursor: true,
|
|
613
|
+
node: CommentView,
|
|
614
|
+
},
|
|
615
|
+
pagination: {
|
|
616
|
+
hasNext: true,
|
|
617
|
+
hasPrevious: true,
|
|
618
|
+
nextCursor: true,
|
|
619
|
+
previousCursor: true,
|
|
620
|
+
},
|
|
621
|
+
};
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
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.
|
|
625
|
+
|
|
626
|
+
Additional arguments on a root request are scoped to that root list:
|
|
627
|
+
|
|
628
|
+
```tsx
|
|
629
|
+
const { posts } = useRequest({
|
|
630
|
+
posts: {
|
|
631
|
+
args: { categoryId: category.id, first: 20 },
|
|
632
|
+
list: PostConnectionView,
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
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.
|
|
638
|
+
|
|
639
|
+
## Live Views
|
|
640
|
+
|
|
641
|
+
`useLiveView` resolves a `ViewRef` just like `useView`, but also keeps the selected object up to date through the native live SSE transport.
|
|
642
|
+
|
|
643
|
+
```tsx
|
|
644
|
+
import { useLiveView, ViewRef } from 'react-fate';
|
|
645
|
+
|
|
646
|
+
export const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
|
|
647
|
+
const post = useLiveView(PostView, postRef);
|
|
648
|
+
|
|
649
|
+
return (
|
|
650
|
+
<Card>
|
|
651
|
+
<h2>{post.title}</h2>
|
|
652
|
+
{/* Updates automatically! */}
|
|
653
|
+
<p>{post.likes} likes</p>
|
|
654
|
+
</Card>
|
|
655
|
+
);
|
|
656
|
+
};
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
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.
|
|
660
|
+
|
|
661
|
+
### How Live Updates Work
|
|
662
|
+
|
|
663
|
+
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.
|
|
664
|
+
|
|
665
|
+
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.
|
|
666
|
+
|
|
667
|
+
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.
|
|
668
|
+
|
|
669
|
+
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.
|
|
670
|
+
|
|
671
|
+
### Client Setup
|
|
672
|
+
|
|
673
|
+
Generate the client with the native transport and point it at your Fate endpoint:
|
|
674
|
+
|
|
675
|
+
```tsx
|
|
676
|
+
import { FateClient } from 'react-fate';
|
|
677
|
+
import { createFateClient } from 'react-fate/client';
|
|
678
|
+
|
|
679
|
+
export function App() {
|
|
680
|
+
const fate = useMemo(
|
|
681
|
+
() =>
|
|
682
|
+
createFateClient({
|
|
683
|
+
fetch: (input, init) =>
|
|
684
|
+
fetch(input, {
|
|
685
|
+
...init,
|
|
686
|
+
credentials: 'include',
|
|
687
|
+
}),
|
|
688
|
+
url: `${env('SERVER_URL')}/fate`,
|
|
689
|
+
}),
|
|
690
|
+
[],
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
return <FateClient client={fate}>{/* Components go here */}</FateClient>;
|
|
694
|
+
}
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
> [!NOTE]
|
|
698
|
+
>
|
|
699
|
+
> Live views use `GET /fate/live` for the single SSE stream and `POST /fate/live` for subscribe/unsubscribe control messages.
|
|
700
|
+
|
|
701
|
+
### Server Setup
|
|
702
|
+
|
|
703
|
+
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.
|
|
704
|
+
|
|
705
|
+
Pass a live event bus to `createFateServer` and expose the native handler:
|
|
706
|
+
|
|
707
|
+
```tsx
|
|
708
|
+
import { createFateServer, createHonoFateHandler, createLiveEventBus } from '@nkzw/fate/server';
|
|
709
|
+
import type { AppContext } from './context.ts';
|
|
710
|
+
import { sources } from './sources.ts';
|
|
711
|
+
import { Root } from './views.ts';
|
|
712
|
+
|
|
713
|
+
export const live = createLiveEventBus();
|
|
714
|
+
|
|
715
|
+
export const fate = createFateServer<AppContext>({
|
|
716
|
+
live,
|
|
717
|
+
roots: Root,
|
|
718
|
+
sources,
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
app.all('/fate/*', createHonoFateHandler(fate));
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
Once this is in place, components can switch from `useView` to `useLiveView` without changing their view definitions or return types.
|
|
725
|
+
|
|
726
|
+
### Live List Views
|
|
727
|
+
|
|
728
|
+
`useLiveListView` mirrors `useListView`, but subscribes to live connection events for the connection it receives:
|
|
729
|
+
|
|
730
|
+
```tsx
|
|
731
|
+
import { useLiveListView, useLiveView, ViewRef } from 'react-fate';
|
|
732
|
+
|
|
733
|
+
export function PostCard({ post: postRef }: { post: ViewRef<'Post'> }) {
|
|
734
|
+
const post = useLiveView(PostView, postRef);
|
|
735
|
+
const [comments, loadNext] = useLiveListView(CommentConnectionView, post.comments);
|
|
736
|
+
|
|
737
|
+
return (
|
|
738
|
+
<>
|
|
739
|
+
{comments.map(({ node }) => (
|
|
740
|
+
<CommentCard comment={node} key={node.id} />
|
|
741
|
+
))}
|
|
742
|
+
{loadNext ? <button onClick={loadNext}>Load more</button> : null}
|
|
743
|
+
</>
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
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.
|
|
749
|
+
|
|
750
|
+
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:
|
|
751
|
+
|
|
752
|
+
```tsx
|
|
753
|
+
const MessageConnectionView = {
|
|
754
|
+
args: { first: 30 },
|
|
755
|
+
items: {
|
|
756
|
+
node: MessageView,
|
|
757
|
+
},
|
|
758
|
+
live: {
|
|
759
|
+
append: 'visible',
|
|
760
|
+
},
|
|
761
|
+
};
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
Emit connection events on the server when list membership changes:
|
|
765
|
+
|
|
766
|
+
```tsx
|
|
767
|
+
live.connection('Post.comments', { id: postId }).prependNode('Comment', comment.id);
|
|
768
|
+
live.connection('Post.comments', { id: postId }).deleteEdge('Comment', comment.id);
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
For root lists, use the generated root procedure name:
|
|
772
|
+
|
|
773
|
+
```tsx
|
|
774
|
+
live.connection('posts', { categoryId }).prependNode('Post', post.id);
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
If the changed list cannot be described precisely, invalidate the active connection and fate will refetch it:
|
|
778
|
+
|
|
779
|
+
```tsx
|
|
780
|
+
live.connection('posts', { categoryId }).invalidate();
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
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.
|
|
784
|
+
|
|
785
|
+
### Emitting Events
|
|
786
|
+
|
|
787
|
+
After a mutation changes an object, emit an update event for that object:
|
|
788
|
+
|
|
789
|
+
```tsx
|
|
790
|
+
export const postRouter = router({
|
|
791
|
+
...fate.procedures({
|
|
792
|
+
view: postDataView,
|
|
793
|
+
}),
|
|
794
|
+
like: procedure.input(likeInput).mutation(async ({ ctx, input }) => {
|
|
795
|
+
const post = await ctx.prisma.post.update({
|
|
796
|
+
data: {
|
|
797
|
+
likes: {
|
|
798
|
+
increment: 1,
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
where: { id: input.id },
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
live.update('Post', input.id);
|
|
805
|
+
|
|
806
|
+
return post;
|
|
807
|
+
}),
|
|
808
|
+
});
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
This tells fate that the `Post` changed. Every active live view for that post refreshes using the selection it subscribed with.
|
|
812
|
+
|
|
813
|
+
If you know which fields changed, pass them with `changed` to reduce the amount of data sent to each subscriber:
|
|
814
|
+
|
|
815
|
+
```tsx
|
|
816
|
+
live.update('Post', input.id, { changed: ['likes'] });
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
With this version, a live view that selected `likes` refreshes only `likes`, while a live view that only selected unrelated fields is skipped entirely.
|
|
820
|
+
|
|
821
|
+
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`:
|
|
822
|
+
|
|
823
|
+
```tsx
|
|
824
|
+
export const commentRouter = router({
|
|
825
|
+
add: procedure.input(addCommentInput).mutation(async ({ ctx, input }) => {
|
|
826
|
+
const comment = await ctx.prisma.comment.create({
|
|
827
|
+
data: {
|
|
828
|
+
content: input.content,
|
|
829
|
+
postId: input.postId,
|
|
830
|
+
},
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
live.update('Post', input.postId, { changed: ['commentCount', 'comments'] });
|
|
834
|
+
|
|
835
|
+
return comment;
|
|
836
|
+
}),
|
|
837
|
+
});
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
For deletions, emit a delete event for the deleted object if clients may be subscribed to it:
|
|
841
|
+
|
|
842
|
+
```tsx
|
|
843
|
+
live.delete('Comment', input.id);
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
If deleting the object also changes another object, emit an update for that object too:
|
|
847
|
+
|
|
848
|
+
```tsx
|
|
849
|
+
live.update('Post', postId, { changed: ['commentCount', 'comments'] });
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
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:
|
|
853
|
+
|
|
854
|
+
```tsx
|
|
855
|
+
live.update('Post', input.id, {
|
|
856
|
+
changed: ['likes'],
|
|
857
|
+
eventId: `post:${input.id}:${Date.now()}`,
|
|
858
|
+
});
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
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.
|
|
862
|
+
|
|
863
|
+
### Error Handling
|
|
864
|
+
|
|
865
|
+
Live subscription errors are reported out of band. They do not replace the last cached data or throw through the component that called `useLiveView`.
|
|
866
|
+
|
|
867
|
+
Pass `onLiveError` when creating the client to send those failures to your logger or monitoring system:
|
|
868
|
+
|
|
869
|
+
```tsx
|
|
870
|
+
const fate = createFateClient({
|
|
871
|
+
fetch: (input, init) =>
|
|
872
|
+
fetch(input, {
|
|
873
|
+
...init,
|
|
874
|
+
credentials: 'include',
|
|
875
|
+
}),
|
|
876
|
+
onLiveError(error) {
|
|
877
|
+
captureException(error);
|
|
878
|
+
},
|
|
879
|
+
url: `${env('SERVER_URL')}/fate`,
|
|
880
|
+
});
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
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.
|
|
884
|
+
|
|
576
885
|
## Actions
|
|
577
886
|
|
|
578
887
|
fate does not provide hooks for mutations like traditional data fetching libraries do. Instead, mutations are exposed in two ways:
|
|
@@ -659,6 +968,24 @@ addComment({
|
|
|
659
968
|
});
|
|
660
969
|
```
|
|
661
970
|
|
|
971
|
+
By default, fate inserts new records after existing items in matching root lists and nested lists. For a newest-first list, pass `insert: 'before'` so optimistic records appear at the beginning:
|
|
972
|
+
|
|
973
|
+
```tsx
|
|
974
|
+
addComment({
|
|
975
|
+
input: { content, postId: post.id },
|
|
976
|
+
insert: 'before',
|
|
977
|
+
optimistic: {
|
|
978
|
+
content,
|
|
979
|
+
id: `optimistic:${Date.now().toString(36)}`,
|
|
980
|
+
post: { id: post.id },
|
|
981
|
+
},
|
|
982
|
+
});
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
Insertion respects pagination boundaries. If you append to a list that still has a next page, fate keeps the new record attached to the unresolved trailing edge instead of mixing it into the loaded page. As you load more pages, the inserted record stays at the end until the server returns the canonical item or the list reaches the edge. The same behavior applies to prepends while `hasPrevious` is true.
|
|
986
|
+
|
|
987
|
+
Multiple pending optimistic inserts keep their visible order. For example, two `insert: 'before'` calls on a newest-first feed show the second optimistic item before the first, matching what users expect from newly created content.
|
|
988
|
+
|
|
662
989
|
### Selecting a View with Actions
|
|
663
990
|
|
|
664
991
|
Mutations may change data that is not directly specified in the mutation result. For example, adding a comment increases the post's comment count. For such cases, you can provide a `view` to an action that specifies which fields to fetch as part of the mutation:
|
|
@@ -821,31 +1148,36 @@ addComment({
|
|
|
821
1148
|
|
|
822
1149
|
## Server Integration
|
|
823
1150
|
|
|
824
|
-
Until now, we have focused on the client-side API of fate. You'll need a
|
|
1151
|
+
Until now, we have focused on the client-side API of fate. You'll need a backend that follows fate's data protocol so the Vite plugin can provide a typed client module. _fate_ currently ships two server paths:
|
|
1152
|
+
|
|
1153
|
+
- The native Fate protocol, which is transport-agnostic and can be hosted by any Fetch-compatible server.
|
|
1154
|
+
- The tRPC adapter, which keeps compatibility with existing tRPC backends.
|
|
1155
|
+
|
|
1156
|
+
_fate_ currently provides database adapters for Prisma and Drizzle, but the framework itself is not coupled to a particular ORM. The adapters plug into the same source execution runtime and can be exposed through the native protocol or through tRPC.
|
|
825
1157
|
|
|
826
1158
|
### Conventions & Object Identity
|
|
827
1159
|
|
|
828
|
-
fate expects that data is served by a
|
|
1160
|
+
fate expects that data is served by a backend that follows these conventions:
|
|
829
1161
|
|
|
830
1162
|
- A `byId` query for each data type to fetch individual objects by their unique identifier (`id`).
|
|
831
1163
|
- A `list` query for fetching lists of objects with support for pagination.
|
|
832
1164
|
|
|
833
1165
|
Objects are identified by their ID and type name (`__typename`, e.g. `Post`, `User`), and stored by `__typename:id` (e.g. "Post:123") in the client cache. fate keeps list orderings under stable keys derived from the backend procedure and args. Relations are stored as IDs and returned to components as ViewRef tokens.
|
|
834
1166
|
|
|
835
|
-
fate's type definitions might seem verbose at first glance. However, with fate's minimal API surface, AI tools can easily generate this code for you
|
|
1167
|
+
fate's type definitions might seem verbose at first glance. However, with fate's minimal API surface, AI tools can easily generate this code for you, or you can let the Vite plugin provide the typed client module for your app.
|
|
836
1168
|
|
|
837
1169
|
> [!NOTE]
|
|
838
1170
|
> You can adopt _fate_ incrementally in an existing tRPC codebase without changing your existing schema by adding these queries alongside your existing procedures.
|
|
839
1171
|
|
|
840
1172
|
### Data Views
|
|
841
1173
|
|
|
842
|
-
To continue with our client example, let's assume we have a `post.ts` file with a tRPC router that exposes a `byId` query for selecting objects by id, and a root `list` query to fetch a list of posts.
|
|
843
|
-
|
|
844
1174
|
Since clients can send arbitrary selection objects to the server, we need to implement a way to translate these selection objects into database queries without exposing raw database queries and private data to the client. On the client, we define views to select fields on each type. We can do the same on the server using fate data views and the `dataView` function from `@nkzw/fate/server`.
|
|
845
1175
|
|
|
846
|
-
Create a `views.ts` file next to your
|
|
1176
|
+
Create a `views.ts` file next to your server entry that exports the data views for each type. The same data view shape works with both Prisma model types and Drizzle row types:
|
|
847
1177
|
|
|
848
|
-
|
|
1178
|
+
::: code-group
|
|
1179
|
+
|
|
1180
|
+
```tsx [Prisma]
|
|
849
1181
|
import { dataView, type Entity } from '@nkzw/fate/server';
|
|
850
1182
|
import type { User as PrismaUser } from '../prisma/prisma-client/client.ts';
|
|
851
1183
|
|
|
@@ -858,37 +1190,21 @@ export const userDataView = dataView<PrismaUser>('User')({
|
|
|
858
1190
|
export type User = Entity<typeof userDataView, 'User'>;
|
|
859
1191
|
```
|
|
860
1192
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
We can apply the above data view in our tRPC router and resolve the client's selection against it using `createResolver`. Here is an example implementation of the `byId` query for the `User` type which allows fetching multiple users by `id`:
|
|
866
|
-
|
|
867
|
-
```tsx
|
|
868
|
-
import { byIdInput, createResolver } from '@nkzw/fate/server';
|
|
869
|
-
import { z } from 'zod';
|
|
870
|
-
import type { UserFindManyArgs } from '../../prisma/prisma-client/models.ts';
|
|
871
|
-
import { procedure, router } from '../init.ts';
|
|
872
|
-
import { userDataView } from '../views.ts';
|
|
873
|
-
|
|
874
|
-
export const userRouter = router({
|
|
875
|
-
byId: procedure.input(byIdInput).query(async ({ ctx, input }) => {
|
|
876
|
-
const { resolveMany, select } = createResolver({
|
|
877
|
-
...input,
|
|
878
|
-
ctx,
|
|
879
|
-
view: userDataView,
|
|
880
|
-
});
|
|
881
|
-
|
|
882
|
-
const users = await ctx.prisma.user.findMany({
|
|
883
|
-
select: select,
|
|
884
|
-
where: { id: { in: input.ids } },
|
|
885
|
-
} as UserFindManyArgs);
|
|
1193
|
+
```tsx [Drizzle]
|
|
1194
|
+
import { dataView, type Entity } from '@nkzw/fate/server';
|
|
1195
|
+
import type { UserRow } from '../drizzle/schema.ts';
|
|
886
1196
|
|
|
887
|
-
|
|
888
|
-
|
|
1197
|
+
export const userDataView = dataView<UserRow>('User')({
|
|
1198
|
+
id: true,
|
|
1199
|
+
name: true,
|
|
1200
|
+
username: true,
|
|
889
1201
|
});
|
|
1202
|
+
|
|
1203
|
+
export type User = Entity<typeof userDataView, 'User'>;
|
|
890
1204
|
```
|
|
891
1205
|
|
|
1206
|
+
:::
|
|
1207
|
+
|
|
892
1208
|
Now that we apply `userDataView` to the `byId` query, the server limits the selection to the fields defined in the data view, keeping private fields hidden from the client, and providing type safety for client views:
|
|
893
1209
|
|
|
894
1210
|
```tsx
|
|
@@ -898,43 +1214,6 @@ const UserData = view<User>()({
|
|
|
898
1214
|
});
|
|
899
1215
|
```
|
|
900
1216
|
|
|
901
|
-
### tRPC List Implementation
|
|
902
|
-
|
|
903
|
-
To implement the `list` query for fetching a paginated list of posts, we can use fate's `createConnectionProcedure` helper. This helper simplifies the implementation of pagination. Here is an example implementation of the `postRouter` with a `list` query:
|
|
904
|
-
|
|
905
|
-
```tsx
|
|
906
|
-
import { createResolver } from '@nkzw/fate/server';
|
|
907
|
-
import type { PostFindManyArgs } from '../../prisma/prisma-client/models.ts';
|
|
908
|
-
import { createConnectionProcedure } from '../connection.ts';
|
|
909
|
-
import { router } from '../init.ts';
|
|
910
|
-
import { postDataView } from '../views.ts';
|
|
911
|
-
|
|
912
|
-
export const postRouter = router({
|
|
913
|
-
list: createConnectionProcedure({
|
|
914
|
-
query: async ({ ctx, cursor, direction, input, skip, take }) => {
|
|
915
|
-
const { resolveMany, select } = createResolver({
|
|
916
|
-
...input,
|
|
917
|
-
ctx,
|
|
918
|
-
view: postDataView,
|
|
919
|
-
});
|
|
920
|
-
const findOptions: PostFindManyArgs = {
|
|
921
|
-
orderBy: { createdAt: 'desc' },
|
|
922
|
-
select,
|
|
923
|
-
take: direction === 'forward' ? take : -take,
|
|
924
|
-
};
|
|
925
|
-
|
|
926
|
-
if (cursor) {
|
|
927
|
-
findOptions.cursor = { id: cursor };
|
|
928
|
-
findOptions.skip = skip;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
const items = await ctx.prisma.post.findMany(findOptions);
|
|
932
|
-
return resolveMany(direction === 'forward' ? items : items.reverse());
|
|
933
|
-
},
|
|
934
|
-
}),
|
|
935
|
-
});
|
|
936
|
-
```
|
|
937
|
-
|
|
938
1217
|
### Data View Composition
|
|
939
1218
|
|
|
940
1219
|
Similar to client-side views, data views can be composed of other data views:
|
|
@@ -962,7 +1241,7 @@ export const commentDataView = dataView<CommentItem>('Comment')({
|
|
|
962
1241
|
|
|
963
1242
|
export const postDataView = dataView<PostItem>('Post')({
|
|
964
1243
|
author: userDataView,
|
|
965
|
-
comments: list(commentDataView),
|
|
1244
|
+
comments: list(commentDataView, { orderBy: [{ createdAt: 'asc' }, { id: 'asc' }] }),
|
|
966
1245
|
});
|
|
967
1246
|
```
|
|
968
1247
|
|
|
@@ -970,15 +1249,20 @@ We can define extra root-level lists and queries by exporting a `Root` object fr
|
|
|
970
1249
|
|
|
971
1250
|
```tsx
|
|
972
1251
|
export const Root = {
|
|
973
|
-
categories: list(categoryDataView),
|
|
974
|
-
commentSearch: {
|
|
975
|
-
|
|
976
|
-
|
|
1252
|
+
categories: list(categoryDataView, { orderBy: [{ createdAt: 'asc' }, { id: 'asc' }] }),
|
|
1253
|
+
commentSearch: {
|
|
1254
|
+
procedure: 'search',
|
|
1255
|
+
view: list(commentDataView, { orderBy: [{ createdAt: 'desc' }, { id: 'desc' }] }),
|
|
1256
|
+
},
|
|
1257
|
+
events: list(eventDataView, { orderBy: [{ startAt: 'asc' }, { id: 'asc' }] }),
|
|
1258
|
+
posts: list(postDataView, { orderBy: { createdAt: 'desc', id: 'desc' } }),
|
|
977
1259
|
viewer: userDataView,
|
|
978
1260
|
};
|
|
979
1261
|
```
|
|
980
1262
|
|
|
981
|
-
Entries that wrap their view in `list(...)` are treated as list resolvers
|
|
1263
|
+
Entries that wrap their view in `list(...)` are treated as list resolvers. In the native protocol, the root key is the operation name sent by the generated client. In the tRPC adapter, `procedure` can point that root at a specific router procedure. If you omit `list(...)`, fate treats the entry as a standard query.
|
|
1264
|
+
|
|
1265
|
+
You can pass default list options such as `orderBy` to `list(...)`. Ordering is scoped to that specific list wrapper: `Root.posts` can order posts by `createdAt desc`, while `categoryDataView.posts` or `postDataView.comments` can choose their own order. If no order is provided, Fate orders by `id asc`. Fate always appends `id asc` as a tie-breaker when no `id` order is present; include `id` yourself when you need a different tie-breaker direction such as `id desc`. Use the array form when ordering by multiple fields so the priority is unambiguous.
|
|
982
1266
|
|
|
983
1267
|
For the above `Root` definitions, you can make the following requests using `useRequest`:
|
|
984
1268
|
|
|
@@ -999,43 +1283,421 @@ const { posts, categories, viewer } = useRequest({
|
|
|
999
1283
|
});
|
|
1000
1284
|
```
|
|
1001
1285
|
|
|
1002
|
-
###
|
|
1286
|
+
### Native Fate Protocol
|
|
1003
1287
|
|
|
1004
|
-
|
|
1288
|
+
The native protocol keeps tRPC optional. Create a source adapter from your ORM integration, pass it to `createFateServer`, and expose the returned server through a Fetch-compatible handler.
|
|
1005
1289
|
|
|
1006
1290
|
```tsx
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1291
|
+
import { createFateServer, createHonoFateHandler } from '@nkzw/fate/server';
|
|
1292
|
+
import { createPrismaSourceAdapter } from '@nkzw/fate/server/prisma';
|
|
1293
|
+
import { Hono } from 'hono';
|
|
1294
|
+
import type { AppContext } from './context.ts';
|
|
1295
|
+
import { prisma } from './prisma.ts';
|
|
1296
|
+
import { Root, userDataView } from './views.ts';
|
|
1297
|
+
|
|
1298
|
+
export { Root } from './views.ts';
|
|
1299
|
+
|
|
1300
|
+
const sources = createPrismaSourceAdapter<AppContext>({
|
|
1301
|
+
prisma: (ctx) => ctx.prisma,
|
|
1302
|
+
views: Root,
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
export const fate = createFateServer({
|
|
1306
|
+
context: async ({ adapterContext }) => ({
|
|
1307
|
+
prisma,
|
|
1308
|
+
request: adapterContext.req.raw,
|
|
1309
|
+
sessionUser: await getSessionUser(adapterContext.req.raw),
|
|
1310
|
+
}),
|
|
1311
|
+
queries: {
|
|
1312
|
+
viewer: {
|
|
1313
|
+
resolve: ({ ctx, select }) =>
|
|
1314
|
+
sources.resolveById({
|
|
1315
|
+
ctx,
|
|
1316
|
+
id: ctx.sessionUser.id,
|
|
1317
|
+
input: { select },
|
|
1318
|
+
view: userDataView,
|
|
1319
|
+
}),
|
|
1320
|
+
},
|
|
1321
|
+
},
|
|
1322
|
+
roots: Root,
|
|
1323
|
+
sources,
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
const app = new Hono();
|
|
1327
|
+
const handler = createHonoFateHandler(fate);
|
|
1328
|
+
|
|
1329
|
+
app.post('/fate', handler);
|
|
1330
|
+
app.post('/fate/live', handler);
|
|
1331
|
+
```
|
|
1332
|
+
|
|
1333
|
+
Configure the Vite plugin with the native transport:
|
|
1334
|
+
|
|
1335
|
+
```tsx
|
|
1336
|
+
import { fate } from 'react-fate/vite';
|
|
1337
|
+
import { defineConfig } from 'vite';
|
|
1338
|
+
|
|
1339
|
+
export default defineConfig({
|
|
1340
|
+
plugins: [
|
|
1341
|
+
fate({
|
|
1342
|
+
module: '@your-org/server/fate.ts',
|
|
1343
|
+
transport: 'native',
|
|
1013
1344
|
}),
|
|
1345
|
+
],
|
|
1346
|
+
});
|
|
1347
|
+
```
|
|
1348
|
+
|
|
1349
|
+
The generated client uses `createHTTPTransport`:
|
|
1350
|
+
|
|
1351
|
+
```tsx
|
|
1352
|
+
import { createFateClient } from 'react-fate/client';
|
|
1353
|
+
|
|
1354
|
+
const client = createFateClient({
|
|
1355
|
+
url: '/fate',
|
|
1356
|
+
});
|
|
1357
|
+
```
|
|
1358
|
+
|
|
1359
|
+
The HTTP transport batches operations issued in the same microtask into one `POST /fate` request. Live views use one `GET /fate/live` SSE stream per Fate client and `POST /fate/live` control messages when views subscribe or unsubscribe.
|
|
1360
|
+
|
|
1361
|
+
#### Custom Queries
|
|
1362
|
+
|
|
1363
|
+
Root query entries such as `viewer` need an explicit resolver because fate cannot infer application-specific behavior like "current user" from a data view:
|
|
1364
|
+
|
|
1365
|
+
```tsx
|
|
1366
|
+
export const fate = createFateServer({
|
|
1367
|
+
context,
|
|
1368
|
+
queries: {
|
|
1369
|
+
viewer: {
|
|
1370
|
+
resolve: ({ ctx, select }) =>
|
|
1371
|
+
sources.resolveById({
|
|
1372
|
+
ctx,
|
|
1373
|
+
id: ctx.sessionUser.id,
|
|
1374
|
+
input: { select },
|
|
1375
|
+
view: userDataView,
|
|
1376
|
+
}),
|
|
1377
|
+
},
|
|
1378
|
+
},
|
|
1379
|
+
roots: Root,
|
|
1380
|
+
sources,
|
|
1381
|
+
});
|
|
1382
|
+
```
|
|
1383
|
+
|
|
1384
|
+
#### Custom Mutations
|
|
1385
|
+
|
|
1386
|
+
Mutations declare the entity type they return and receive the selected fields requested by the client. Resolve the updated record through the source adapter so the response has the same masking and relation behavior as regular view requests:
|
|
1387
|
+
|
|
1388
|
+
```tsx
|
|
1389
|
+
export const fate = createFateServer({
|
|
1390
|
+
mutations: {
|
|
1391
|
+
'post.like': {
|
|
1392
|
+
input: likeInput,
|
|
1393
|
+
resolve: async ({ ctx, input, select }) => {
|
|
1394
|
+
await ctx.prisma.post.update({
|
|
1395
|
+
data: { likes: { increment: 1 } },
|
|
1396
|
+
where: { id: input.id },
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
return sources.resolveById({
|
|
1400
|
+
ctx,
|
|
1401
|
+
id: input.id,
|
|
1402
|
+
input: { select },
|
|
1403
|
+
view: postDataView,
|
|
1404
|
+
});
|
|
1405
|
+
},
|
|
1406
|
+
type: 'Post',
|
|
1407
|
+
},
|
|
1408
|
+
},
|
|
1409
|
+
roots: Root,
|
|
1410
|
+
sources,
|
|
1411
|
+
});
|
|
1412
|
+
```
|
|
1413
|
+
|
|
1414
|
+
#### Live Views
|
|
1415
|
+
|
|
1416
|
+
Pass a live event bus to enable `useLiveView` over the native SSE endpoint:
|
|
1417
|
+
|
|
1418
|
+
```tsx
|
|
1419
|
+
import { createLiveEventBus } from '@nkzw/fate/server';
|
|
1420
|
+
|
|
1421
|
+
export const live = createLiveEventBus();
|
|
1422
|
+
|
|
1423
|
+
export const fate = createFateServer({
|
|
1424
|
+
live,
|
|
1425
|
+
queries: {
|
|
1426
|
+
viewer: {
|
|
1427
|
+
resolve: ({ ctx, select }) =>
|
|
1428
|
+
sources.resolveById({
|
|
1429
|
+
ctx,
|
|
1430
|
+
id: ctx.sessionUser.id,
|
|
1431
|
+
input: { select },
|
|
1432
|
+
view: userDataView,
|
|
1433
|
+
}),
|
|
1434
|
+
},
|
|
1435
|
+
},
|
|
1436
|
+
roots: Root,
|
|
1437
|
+
sources,
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
live.update('Post', post.id, {
|
|
1441
|
+
changed: ['likes'],
|
|
1442
|
+
eventId: `post:${post.id}:${Date.now()}`,
|
|
1443
|
+
});
|
|
1444
|
+
```
|
|
1445
|
+
|
|
1446
|
+
`changed` is optional. When provided, fate resolves only the changed fields selected by each live subscription and skips subscriptions that do not select those fields. `createLiveEventBus` is an in-memory fanout bus. It forwards `eventId` to SSE clients, but it does not replay events after reconnects. If your app needs lossless reconnect behavior, provide a durable live bus implementation that uses the `lastEventId` passed to `listen`, `listenConnection`, `subscribe`, and `subscribeConnection`.
|
|
1447
|
+
|
|
1448
|
+
### tRPC Fate Setup
|
|
1449
|
+
|
|
1450
|
+
The Prisma and Drizzle tRPC integrations connect your data views to your database, bind fate's standard tRPC procedures, and expose helpers for custom queries and mutations.
|
|
1451
|
+
|
|
1452
|
+
Pass the `Root` export from `views.ts` to Fate in your tRPC `init.ts` file. Fate walks that view graph to find the data views it needs. `id` defaults to `"id"`, and Fate uses it as the fallback ordering for cursor pagination. Relations are inferred from the data view and ORM schema: a nested data view is loaded as a singular relation, `list(view)` is loaded as a list relation, and Drizzle join tables are discovered from relation metadata.
|
|
1453
|
+
|
|
1454
|
+
#### Prisma
|
|
1455
|
+
|
|
1456
|
+
Use `createPrismaFate` from `@nkzw/fate/server/prisma` next to your tRPC helpers. By default, Fate reads Prisma delegates from `ctx.prisma` using each data view's type name:
|
|
1457
|
+
|
|
1458
|
+
```tsx
|
|
1459
|
+
import { initTRPC } from '@trpc/server';
|
|
1460
|
+
import { createPrismaFate } from '@nkzw/fate/server/prisma';
|
|
1461
|
+
import type { AppContext } from './context.ts';
|
|
1462
|
+
import { Root } from './views.ts';
|
|
1463
|
+
|
|
1464
|
+
const t = initTRPC.context<AppContext>().create();
|
|
1465
|
+
|
|
1466
|
+
export const router = t.router;
|
|
1467
|
+
export const procedure = t.procedure;
|
|
1468
|
+
|
|
1469
|
+
export const fate = createPrismaFate<AppContext, typeof procedure>({
|
|
1470
|
+
procedure,
|
|
1471
|
+
views: Root,
|
|
1472
|
+
});
|
|
1473
|
+
```
|
|
1474
|
+
|
|
1475
|
+
If your Prisma client is not stored at `ctx.prisma`, pass `prisma: (ctx) => ctx.db`.
|
|
1476
|
+
|
|
1477
|
+
The Prisma integration translates view requests into Prisma `select`, `where`, `cursor`, `skip`, and `take` options. It also hydrates computed `count(...)` dependencies using Prisma `groupBy` when needed.
|
|
1478
|
+
|
|
1479
|
+
For custom Prisma queries and mutations, use `fate.createPlan` with `toPrismaSelect`:
|
|
1480
|
+
|
|
1481
|
+
```tsx
|
|
1482
|
+
import { toPrismaSelect } from '@nkzw/fate/server';
|
|
1483
|
+
|
|
1484
|
+
const plan = fate.createPlan({
|
|
1485
|
+
...input,
|
|
1486
|
+
ctx,
|
|
1487
|
+
view: postDataView,
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
const post = await ctx.prisma.post.update({
|
|
1491
|
+
data: {
|
|
1492
|
+
likes: {
|
|
1493
|
+
increment: 1,
|
|
1494
|
+
},
|
|
1495
|
+
},
|
|
1496
|
+
select: toPrismaSelect(plan),
|
|
1497
|
+
where: { id: input.id },
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
return plan.resolve(post);
|
|
1501
|
+
```
|
|
1502
|
+
|
|
1503
|
+
#### Drizzle
|
|
1504
|
+
|
|
1505
|
+
Use `createDrizzleFate` from `@nkzw/fate/server/drizzle`. Fate matches data view type names to Drizzle tables from your schema. The `db` option can be a Drizzle database object or a function that receives your tRPC context and returns a request-scoped database object:
|
|
1506
|
+
|
|
1507
|
+
```tsx
|
|
1508
|
+
import { initTRPC } from '@trpc/server';
|
|
1509
|
+
import { createDrizzleFate } from '@nkzw/fate/server/drizzle';
|
|
1510
|
+
import db from '../drizzle/db.ts';
|
|
1511
|
+
import schema from '../drizzle/schema.ts';
|
|
1512
|
+
import type { AppContext } from './context.ts';
|
|
1513
|
+
import { Root } from './views.ts';
|
|
1514
|
+
|
|
1515
|
+
const t = initTRPC.context<AppContext>().create();
|
|
1516
|
+
|
|
1517
|
+
export const router = t.router;
|
|
1518
|
+
export const procedure = t.procedure;
|
|
1519
|
+
|
|
1520
|
+
export const fate = createDrizzleFate<AppContext, typeof procedure>({
|
|
1521
|
+
db,
|
|
1522
|
+
procedure,
|
|
1523
|
+
schema,
|
|
1524
|
+
views: Root,
|
|
1525
|
+
});
|
|
1526
|
+
```
|
|
1527
|
+
|
|
1528
|
+
If your database lives on the request context, pass a function instead:
|
|
1529
|
+
|
|
1530
|
+
```tsx
|
|
1531
|
+
import schema from '../drizzle/schema.ts';
|
|
1532
|
+
import { Root } from './views.ts';
|
|
1533
|
+
|
|
1534
|
+
export const fate = createDrizzleFate<AppContext, typeof procedure>({
|
|
1535
|
+
db: (ctx) => ctx.db,
|
|
1536
|
+
procedure,
|
|
1537
|
+
schema,
|
|
1538
|
+
views: Root,
|
|
1539
|
+
});
|
|
1540
|
+
```
|
|
1541
|
+
|
|
1542
|
+
The Drizzle adapter builds SQL queries from your registered data views. It selects only requested columns, hydrates singular, list, and many-to-many relations, supports nested cursor pagination, and hydrates computed `count(...)` dependencies with SQL grouped counts. Count filters may be plain equality objects or Drizzle SQL predicates written as `(columns) => eq(columns.status, 'GOING')`.
|
|
1543
|
+
|
|
1544
|
+
For request-specific sorting, prefer a custom root query that validates and translates explicit sort args.
|
|
1545
|
+
|
|
1546
|
+
For many-to-many relations, define the join table relations in your Drizzle schema. Fate discovers a join table that points at both the source table and the target table:
|
|
1547
|
+
|
|
1548
|
+
```tsx
|
|
1549
|
+
export const fate = createDrizzleFate<AppContext, typeof procedure>({
|
|
1550
|
+
db,
|
|
1551
|
+
procedure,
|
|
1552
|
+
schema,
|
|
1553
|
+
views: Root,
|
|
1554
|
+
});
|
|
1555
|
+
```
|
|
1556
|
+
|
|
1557
|
+
You can still provide explicit join metadata when the schema is ambiguous:
|
|
1558
|
+
|
|
1559
|
+
```tsx
|
|
1560
|
+
{
|
|
1561
|
+
manyToMany: {
|
|
1562
|
+
tags: {
|
|
1563
|
+
foreignColumn: postToTag.tagId,
|
|
1564
|
+
localColumn: postToTag.postId,
|
|
1565
|
+
table: postToTag,
|
|
1566
|
+
},
|
|
1567
|
+
},
|
|
1568
|
+
relations: {
|
|
1569
|
+
tags: {
|
|
1570
|
+
foreignKey: 'id',
|
|
1571
|
+
localKey: 'id',
|
|
1572
|
+
through: {
|
|
1573
|
+
foreignKey: 'tagId',
|
|
1574
|
+
localKey: 'postId',
|
|
1575
|
+
},
|
|
1576
|
+
},
|
|
1577
|
+
},
|
|
1578
|
+
table: post,
|
|
1579
|
+
view: postDataView,
|
|
1580
|
+
}
|
|
1581
|
+
```
|
|
1582
|
+
|
|
1583
|
+
Drizzle writes should stay ordinary Drizzle code. After creating or updating a row, use `fate.resolveById` to return the selected shape that the client asked for:
|
|
1584
|
+
|
|
1585
|
+
```tsx
|
|
1586
|
+
const postId = await createPostRecord({
|
|
1587
|
+
authorId: ctx.sessionUser.id,
|
|
1588
|
+
content: input.content,
|
|
1589
|
+
title: input.title,
|
|
1590
|
+
});
|
|
1591
|
+
|
|
1592
|
+
const post = await fate.resolveById({
|
|
1593
|
+
ctx,
|
|
1594
|
+
id: postId,
|
|
1595
|
+
input,
|
|
1596
|
+
view: postDataView,
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
return post;
|
|
1600
|
+
```
|
|
1601
|
+
|
|
1602
|
+
### tRPC Procedures
|
|
1603
|
+
|
|
1604
|
+
Use `fate.procedures` to build the standard `byId` and `list` procedures expected by the generated fate client:
|
|
1605
|
+
|
|
1606
|
+
```tsx
|
|
1607
|
+
import { fate, router } from '../init.ts';
|
|
1608
|
+
import { postDataView } from '../views.ts';
|
|
1609
|
+
|
|
1610
|
+
export const postRouter = router({
|
|
1611
|
+
...fate.procedures(postDataView),
|
|
1612
|
+
});
|
|
1613
|
+
```
|
|
1614
|
+
|
|
1615
|
+
You can disable the generated `list` procedure if a view should only be fetched by id:
|
|
1616
|
+
|
|
1617
|
+
```tsx
|
|
1618
|
+
export const commentRouter = router({
|
|
1619
|
+
...fate.procedures({
|
|
1620
|
+
list: false,
|
|
1621
|
+
view: commentDataView,
|
|
1622
|
+
}),
|
|
1623
|
+
});
|
|
1624
|
+
```
|
|
1625
|
+
|
|
1626
|
+
### Custom Queries
|
|
1627
|
+
|
|
1628
|
+
You can add custom root queries next to generated procedures. Define the root in `Root`, implement a matching tRPC procedure, and call `fate.resolveConnection`:
|
|
1629
|
+
|
|
1630
|
+
```tsx
|
|
1631
|
+
export const Root = {
|
|
1632
|
+
commentSearch: { procedure: 'search', view: list(commentDataView) },
|
|
1633
|
+
};
|
|
1634
|
+
```
|
|
1635
|
+
|
|
1636
|
+
```tsx
|
|
1637
|
+
import { ilike } from 'drizzle-orm';
|
|
1638
|
+
import { fate } from '../init.ts';
|
|
1639
|
+
|
|
1640
|
+
export const commentRouter = router({
|
|
1641
|
+
...fate.procedures({
|
|
1642
|
+
list: false,
|
|
1643
|
+
view: commentDataView,
|
|
1644
|
+
}),
|
|
1645
|
+
search: fate.connection({
|
|
1646
|
+
input: z.object({
|
|
1647
|
+
query: z.string().min(1, 'Search query is required'),
|
|
1648
|
+
}),
|
|
1649
|
+
query: ({ ctx, cursor, direction, input, take }) =>
|
|
1650
|
+
fate.resolveConnection({
|
|
1651
|
+
ctx,
|
|
1652
|
+
cursor,
|
|
1653
|
+
direction,
|
|
1654
|
+
extra: {
|
|
1655
|
+
where: ilike(comment.content, `%${input.args.query}%`),
|
|
1656
|
+
},
|
|
1657
|
+
input,
|
|
1658
|
+
take,
|
|
1659
|
+
view: commentDataView,
|
|
1660
|
+
}),
|
|
1014
1661
|
}),
|
|
1015
|
-
comments: list(commentDataView),
|
|
1016
|
-
id: true,
|
|
1017
1662
|
});
|
|
1018
1663
|
```
|
|
1019
1664
|
|
|
1020
|
-
|
|
1665
|
+
For Prisma, pass Prisma query options such as `{ where: { ... } }` in `extra` instead of a Drizzle SQL expression.
|
|
1021
1666
|
|
|
1022
|
-
###
|
|
1667
|
+
### Data View Resolvers
|
|
1023
1668
|
|
|
1024
|
-
|
|
1669
|
+
fate data views support computed fields. Use `computed`, `field`, and `count` to describe the hidden data needed to resolve a public field:
|
|
1025
1670
|
|
|
1026
1671
|
```tsx
|
|
1672
|
+
import { computed, count, field } from '@nkzw/fate/server';
|
|
1673
|
+
|
|
1027
1674
|
export const userDataView = dataView<UserItem>('User')({
|
|
1028
|
-
email:
|
|
1029
|
-
authorize: ({ id }, context) => context?.
|
|
1030
|
-
|
|
1675
|
+
email: computed<UserItem, string | null, AppContext>({
|
|
1676
|
+
authorize: ({ id }, context) => context?.sessionUser?.id === id,
|
|
1677
|
+
select: {
|
|
1678
|
+
email: field('email'),
|
|
1679
|
+
},
|
|
1680
|
+
resolve: (_item, deps) => (deps.email as string | null) ?? null,
|
|
1681
|
+
}),
|
|
1682
|
+
id: true,
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
export const postDataView = dataView<PostItem>('Post')({
|
|
1686
|
+
commentCount: computed<PostItem, number>({
|
|
1687
|
+
select: {
|
|
1688
|
+
count: count('comments'),
|
|
1689
|
+
},
|
|
1690
|
+
resolve: (_item, deps) => (deps.count as number) ?? 0,
|
|
1031
1691
|
}),
|
|
1032
1692
|
id: true,
|
|
1033
1693
|
});
|
|
1034
1694
|
```
|
|
1035
1695
|
|
|
1036
|
-
|
|
1696
|
+
The adapters fetch the hidden `field(...)` and `count(...)` dependencies for you. This keeps private fields like `email` available to the resolver without exposing them to the client selection.
|
|
1037
1697
|
|
|
1038
|
-
|
|
1698
|
+
### Configuring the typed client
|
|
1699
|
+
|
|
1700
|
+
Now that we have defined our client views and our tRPC server, we need to connect them with some glue code. We recommend using fate's Vite plugin for convenience.
|
|
1039
1701
|
|
|
1040
1702
|
First, make sure our tRPC `router.ts` file exports the `appRouter` object, `AppRouter` type and all the views we have defined:
|
|
1041
1703
|
|
|
@@ -1054,22 +1716,43 @@ export type AppRouter = typeof appRouter;
|
|
|
1054
1716
|
export * from './views.ts';
|
|
1055
1717
|
```
|
|
1056
1718
|
|
|
1057
|
-
|
|
1719
|
+
Configure the fate Vite plugin with your server module:
|
|
1058
1720
|
|
|
1059
|
-
```
|
|
1060
|
-
|
|
1721
|
+
```tsx
|
|
1722
|
+
import { fate } from 'react-fate/vite';
|
|
1723
|
+
import { defineConfig } from 'vite';
|
|
1724
|
+
|
|
1725
|
+
export default defineConfig({
|
|
1726
|
+
plugins: [
|
|
1727
|
+
fate({
|
|
1728
|
+
module: '@your-org/server/trpc/router.ts',
|
|
1729
|
+
}),
|
|
1730
|
+
],
|
|
1731
|
+
});
|
|
1061
1732
|
```
|
|
1062
1733
|
|
|
1063
|
-
_Note: fate uses the specified server module name to extract the server types it needs and uses the same module name
|
|
1734
|
+
_Note: fate uses the specified server module name to extract the server types it needs and uses the same module name in the generated client. Make sure that the module is available to the client package's Vite config._
|
|
1735
|
+
|
|
1736
|
+
During development, the plugin watches the server module and the files it imports. When one of those files changes, fate regenerates the project-local client and invalidates `@nkzw/fate/client` in Vite's module graph.
|
|
1737
|
+
|
|
1738
|
+
For a barebones client without React, import the plugin from `@nkzw/fate/vite` and the generated client from `@nkzw/fate/client`. The plugin writes the project-local client for the selected client module path.
|
|
1739
|
+
|
|
1740
|
+
The plugin writes project-local types under `.fate/`. If your TypeScript config does not already include dot-directories, extend the generated config:
|
|
1741
|
+
|
|
1742
|
+
```json
|
|
1743
|
+
{
|
|
1744
|
+
"extends": "./.fate/tsconfig.json"
|
|
1745
|
+
}
|
|
1746
|
+
```
|
|
1064
1747
|
|
|
1065
1748
|
### Creating a _fate_ Client
|
|
1066
1749
|
|
|
1067
|
-
Now that
|
|
1750
|
+
Now that the Vite plugin provides the client types, all that remains is creating an instance of the fate client, and using it in our React app using the `FateClient` context provider:
|
|
1068
1751
|
|
|
1069
1752
|
```tsx
|
|
1070
1753
|
import { httpBatchLink } from '@trpc/client';
|
|
1071
1754
|
import { FateClient } from 'react-fate';
|
|
1072
|
-
import { createFateClient } from '
|
|
1755
|
+
import { createFateClient } from 'react-fate/client';
|
|
1073
1756
|
|
|
1074
1757
|
export function App() {
|
|
1075
1758
|
const fate = useMemo(
|
|
@@ -1100,7 +1783,7 @@ _And you are all set. Happy building!_
|
|
|
1100
1783
|
|
|
1101
1784
|
[In an alternate reality](https://github.com/phacility/javelin), _fate_ can be described like this:
|
|
1102
1785
|
|
|
1103
|
-
**_fate_** is an ambitious React data library that tries to blend Relay-style ideas with
|
|
1786
|
+
**_fate_** is an ambitious React data library that tries to blend Relay-style ideas with type-safe data fetching, held together by equal parts vision and vibes. It aims to fix problems you definitely wouldn't have if you enjoy writing the same fetch logic in three different places with imperative loading state and error handling. fate promises predictable data flow, minimal APIs, and "no magic", though you may occasionally suspect otherwise.
|
|
1104
1787
|
|
|
1105
1788
|
**_fate_** is almost certainly worse than actual sync engines, but will hopefully be better than existing React data-fetching libraries eventually. Use it if you have a high tolerance for pain and want to help shape the future of data fetching in React.
|
|
1106
1789
|
|
|
@@ -1121,14 +1804,12 @@ Probably. One day. _Maybe._
|
|
|
1121
1804
|
|
|
1122
1805
|
## Future
|
|
1123
1806
|
|
|
1124
|
-
**_fate_** is not complete yet. The
|
|
1807
|
+
**_fate_** is not complete yet. The current implementation of _fate_ ships with tRPC, Prisma, and Drizzle support, but the core ideas are not tied to a particular transport or database. We welcome contributions and ideas to improve fate. Here are some features we'd like to add:
|
|
1125
1808
|
|
|
1126
|
-
-
|
|
1127
|
-
-
|
|
1809
|
+
- Live views for pagination
|
|
1810
|
+
- Additional backend adapters
|
|
1128
1811
|
- Persistent storage for offline support
|
|
1129
|
-
- Implement garbage collection for the cache
|
|
1130
1812
|
- Better code generation and less type repetition
|
|
1131
|
-
- Support for live views and real-time updates via `useLiveView` and SSE
|
|
1132
1813
|
|
|
1133
1814
|
## Acknowledgements
|
|
1134
1815
|
|