react-fate 0.1.3 → 1.0.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.
Files changed (38) hide show
  1. package/README.md +810 -129
  2. package/docs/api/functions/FateClient.md +23 -0
  3. package/docs/api/functions/clientRoot.md +27 -0
  4. package/docs/api/functions/createClient.md +21 -0
  5. package/docs/api/functions/createHTTPTransport.md +51 -0
  6. package/docs/api/functions/createTRPCTransport.md +46 -0
  7. package/docs/api/functions/mutation.md +32 -0
  8. package/docs/api/functions/useFateClient.md +17 -0
  9. package/docs/api/functions/useListView.md +28 -0
  10. package/docs/api/functions/useLiveListView.md +28 -0
  11. package/docs/api/functions/useLiveView.md +38 -0
  12. package/docs/api/functions/useRequest.md +38 -0
  13. package/docs/api/functions/useView.md +37 -0
  14. package/docs/api/functions/view.md +26 -0
  15. package/docs/api/index.md +35 -0
  16. package/docs/api/type-aliases/ConnectionRef.md +13 -0
  17. package/docs/api/type-aliases/InferFateAPI.md +11 -0
  18. package/docs/api/type-aliases/ViewRef.md +13 -0
  19. package/docs/api/variables/toEntityId.md +21 -0
  20. package/docs/guide/actions.md +263 -0
  21. package/docs/guide/core-concepts.md +22 -0
  22. package/docs/guide/getting-started.md +57 -0
  23. package/docs/guide/list-views.md +86 -0
  24. package/docs/guide/live-views.md +245 -0
  25. package/docs/guide/requests.md +130 -0
  26. package/docs/guide/server-integration.md +630 -0
  27. package/docs/guide/views.md +278 -0
  28. package/docs/guide/void-integration.md +167 -0
  29. package/docs/index.md +4 -0
  30. package/lib/cli.d.mts +1 -1
  31. package/lib/cli.mjs +1 -2
  32. package/lib/client.d.mts +7 -0
  33. package/lib/client.mjs +2 -0
  34. package/lib/index.d.mts +41 -17
  35. package/lib/index.mjs +96 -46
  36. package/lib/vite.d.mts +12 -0
  37. package/lib/vite.mjs +8 -0
  38. package/package.json +17 -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 and tRPC inspired by [Relay](https://relay.dev/) and [GraphQL](https://graphql.org/). It combines view composition, normalized caching, data masking, Async React features, and tRPC's type safety.
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 & tRPC
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 puts them on top of tRPC. You get the best of both worlds: type safety between the client and server, and GraphQL-like ergonomics for data fetching. Using _fate_ usually looks like this:
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 get started with [a ready-made template](https://github.com/nkzw-tech/fate-template#readme)._
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
- Get started with [a ready-made template](https://github.com/nkzw-tech/fate-template#readme) quickly:
65
+ Create a new fate app with Vite+:
65
66
 
66
- ::: code-group
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 your client you need to install `react-fate`:
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
- And for your server, install the core `@nkzw/fate` package:
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 tRPC backend that follows some conventions so you can generate a typed client using fate's CLI. At the moment _fate_ is designed to work with tRPC and Prisma, but the framework is not coupled to any particular ORM or database, it's just what we are starting with.
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 tRPC backend that follows these conventions:
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. For example, fate has a minimal CLI that generates types for the client, but you can also let your LLM write it by hand if you prefer.
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 root tRPC router that exports the data views for each type. Here is how you can define a `User` data view for Prisma's `User` model:
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
- ```tsx
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
- _Note: Currently, fate provides helpers to integrate with Prisma, but the framework is not coupled to any particular ORM or database. We hope to provide more direct integrations in the future, and are always open to contributions._
862
-
863
- ### tRPC Router Implementation
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
- return await resolveMany(users);
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: { procedure: 'search', view: list(commentDataView) },
975
- events: list(eventDataView),
976
- posts: list(postDataView),
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 and use the `procedure` name when calling the corresponding router procedure, defaulting to `list`. If you omit `list(...)`, fate treats the entry as a standard query and uses the view type name to infer the router name.
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
- ### Data View Resolvers
1286
+ ### Native Fate Protocol
1003
1287
 
1004
- fate data views support resolvers for computed fields. If we want to add a `commentCount` field to our `Post` data view, we can use the `resolver` helper that defines a Prisma selection for the database query together with a `resolve` function:
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
- export const postDataView = dataView<PostItem>('Post')({
1008
- author: userDataView,
1009
- commentCount: resolver<PostItem, number>({
1010
- resolve: ({ _count }) => _count?.comments ?? 0,
1011
- select: () => ({
1012
- _count: { select: { comments: true } },
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
- This definition makes the `commentCount` field available to your client-side views.
1665
+ For Prisma, pass Prisma query options such as `{ where: { ... } }` in `extra` instead of a Drizzle SQL expression.
1021
1666
 
1022
- ### Authorization in Resolvers
1667
+ ### Data View Resolvers
1023
1668
 
1024
- You might want to restrict access to certain fields based on the current user or other contextual information. You can do this by adding an `authorize` function to your resolver definition:
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: resolver<UserItem, string | null, { sessionUser: string }>({
1029
- authorize: ({ id }, context) => context?.sessionUserId === id,
1030
- resolve: ({ email }) => email,
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
- ### Generating a typed client
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
- 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 CLI for convenience.
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
- _Note: We try to keep magic to a minimum and you can handwrite the [generated client](https://github.com/nkzw-tech/fate/blob/main/example/client/src/fate.ts) if you prefer._
1719
+ Configure the fate Vite plugin with your server module:
1058
1720
 
1059
- ```bash
1060
- pnpm fate generate @your-org/server/trpc/router.ts client/src/fate.ts
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 to import the views into the generated client. Make sure that the module is available both at the root where you are running the CLI and in the client package._
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 we have generated 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:
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 './fate.ts';
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 tRPC, 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.
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 library lacks core features such as garbage collection, a compiler to extract view definitions statically ahead of time, and there is too much backend boilerplate. The current implementation of _fate_ is not tied to tRPC or Prisma, those are just the ones we are starting with. We welcome contributions and ideas to improve fate. Here are some features we'd like to add:
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
- - Support for Drizzle
1127
- - Support backends other than tRPC
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