react-fate 1.0.3 → 1.2.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 (44) hide show
  1. package/README.md +1220 -147
  2. package/docs/api/functions/FateClient.md +1 -1
  3. package/docs/api/functions/clientRoot.md +1 -1
  4. package/docs/api/functions/createClient.md +8 -4
  5. package/docs/api/functions/createGraphQLTransport.md +21 -0
  6. package/docs/api/functions/createHTTPTransport.md +1 -1
  7. package/docs/api/functions/createTRPCTransport.md +1 -1
  8. package/docs/api/functions/defer.md +21 -0
  9. package/docs/api/functions/graphqlMutation.md +39 -0
  10. package/docs/api/functions/mutation.md +1 -1
  11. package/docs/api/functions/useFateClient.md +1 -1
  12. package/docs/api/functions/useListView.md +4 -4
  13. package/docs/api/functions/useLiveListView.md +4 -4
  14. package/docs/api/functions/useLiveView.md +52 -11
  15. package/docs/api/functions/useRequest.md +1 -1
  16. package/docs/api/functions/useView.md +85 -11
  17. package/docs/api/functions/view.md +1 -1
  18. package/docs/api/index.md +13 -0
  19. package/docs/api/type-aliases/ConnectionRef.md +2 -2
  20. package/docs/api/type-aliases/Deferred.md +19 -0
  21. package/docs/api/type-aliases/FateDehydratedState.md +11 -0
  22. package/docs/api/type-aliases/GraphQLMutationDefinition.md +19 -0
  23. package/docs/api/type-aliases/GraphQLMutationInput.md +11 -0
  24. package/docs/api/type-aliases/GraphQLMutationMap.md +11 -0
  25. package/docs/api/type-aliases/GraphQLMutationOutput.md +11 -0
  26. package/docs/api/type-aliases/GraphQLTransportOptions.md +119 -0
  27. package/docs/api/type-aliases/HydrateOptions.md +7 -0
  28. package/docs/api/type-aliases/HydrationLimits.md +7 -0
  29. package/docs/api/type-aliases/InferFateAPI.md +1 -1
  30. package/docs/api/type-aliases/Pagination.md +39 -0
  31. package/docs/api/type-aliases/ViewRef.md +1 -1
  32. package/docs/api/variables/toEntityId.md +1 -1
  33. package/docs/guide/actions.md +7 -7
  34. package/docs/guide/deferred-views.md +62 -0
  35. package/docs/guide/getting-started.md +21 -3
  36. package/docs/guide/requests.md +52 -0
  37. package/docs/guide/vue.md +256 -0
  38. package/docs/index.md +1 -0
  39. package/docs/integrations/graphql.md +345 -0
  40. package/docs/{guide/server-integration.md → integrations/server.md} +94 -7
  41. package/docs/{guide/void-integration.md → integrations/void.md} +94 -12
  42. package/lib/index.d.mts +20 -15
  43. package/lib/index.mjs +110 -16
  44. package/package.json +5 -5
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- <!-- auto-generated from docs/guide/*.md. Do not edit directly. -->
1
+ <!-- auto-generated from docs/guide/*.md and docs/integrations/*.md. Do not edit directly. -->
2
2
 
3
3
  <p align="center">
4
4
  <picture>
@@ -68,11 +68,11 @@ Create a new fate app with Vite+:
68
68
  vp create fate my-app
69
69
  ```
70
70
 
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.
71
+ The template selector can create a React or Vue client for a Void app with Drizzle, a tRPC app with Drizzle or Prisma, a GraphQL app with Prisma, or a fate client for an existing GraphQL server. React is the default UI framework; pass `--framework vue` or choose Vue in the template selector to create a Vue app. 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.
72
72
 
73
73
  ### Manual Installation
74
74
 
75
- **_fate_** requires React 19.2+. For a React client, install `react-fate`:
75
+ For a React client, install `react-fate`. It requires React 19.2+:
76
76
 
77
77
  ::: code-group
78
78
 
@@ -90,7 +90,25 @@ yarn add react-fate
90
90
 
91
91
  :::
92
92
 
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:
93
+ For a Vue client, install `vue-fate`:
94
+
95
+ ::: code-group
96
+
97
+ ```bash [npm]
98
+ npm add vue-fate
99
+ ```
100
+
101
+ ```bash [pnpm]
102
+ pnpm add vue-fate
103
+ ```
104
+
105
+ ```bash [yarn]
106
+ yarn add vue-fate
107
+ ```
108
+
109
+ :::
110
+
111
+ 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 a framework adapter:
94
112
 
95
113
  ::: code-group
96
114
 
@@ -549,6 +567,121 @@ try {
549
567
 
550
568
  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
569
 
570
+ ### SSR and Hydration
571
+
572
+ Create a request-scoped fate client on the server, preload the route data, and dehydrate its normalized cache:
573
+
574
+ ```tsx
575
+ const fate = createFateClient();
576
+ await fate.request({ post: { id: '12', view: PostView } });
577
+
578
+ return {
579
+ fate: fate.dehydrate(),
580
+ };
581
+ ```
582
+
583
+ Transport the returned value through your framework's loader serialization, React Server Component props, or a safely escaped JSON bootstrap script. The snapshot contains plain serializable values, so serializers such as Seroval can carry it without fate-specific integration. Treat the snapshot as opaque: hydrate it through fate rather than reading or editing its internal data.
584
+
585
+ On the browser, hydrate the new client before rendering components that call `useRequest`:
586
+
587
+ ```tsx
588
+ const fate = createFateClient();
589
+ fate.hydrate(loaderData.fate);
590
+
591
+ hydrateRoot(
592
+ document,
593
+ <FateClient client={fate}>
594
+ <App />
595
+ </FateClient>,
596
+ );
597
+ ```
598
+
599
+ Hydrated `cache-first` requests resolve from the normalized cache without refetching. Hydration restores records, selected-field coverage, root queries, and list pagination state. It intentionally does not restore active requests, subscriptions, retainers, timers, or optimistic mutation state.
600
+
601
+ Snapshots carry a hydration scope and are rejected by clients with a different scope. Generated clients set a stable scope automatically. When constructing a client directly, pass `hydrationScope` and rotate it when deploying an incompatible cache schema or when separating cache namespaces:
602
+
603
+ ```tsx
604
+ const fate = createClient({
605
+ hydrationScope: 'storefront-v2',
606
+ // ...
607
+ });
608
+ ```
609
+
610
+ Use `hydrationLimits` when an application needs stricter bootstrap payload limits. fate applies conservative defaults for total encoded values, collection sizes, and string lengths.
611
+
612
+ By default, hydration preserves values already present in the browser cache while adding missing server data. Pass `{ merge: 'replace' }` only when the snapshot should authoritatively reset the durable cache:
613
+
614
+ ```tsx
615
+ fate.hydrate(loaderData.fate, { merge: 'replace' });
616
+ ```
617
+
618
+ `preserve-existing` recursively combines plain scalar objects while keeping browser values on conflicts. Arrays, dates, entity references, and list windows are atomic: an existing browser value wins as a whole. Replaying a snapshot is safe and does not notify subscribers when durable cache state is unchanged.
619
+
620
+ Do not reuse request-scoped snapshots across users. Dehydrate after awaited route preloading: snapshots are point-in-time values and do not stream cache patches for data that resolves later. Hydration and dehydration reject clients with in-flight requests, so hydrate the initial snapshot before rendering.
621
+
622
+ ## Deferred Views
623
+
624
+ Use `defer` when a field should not block the parent view. The parent view receives a deferred handle immediately after the eager fields are available, and the component that reads that handle with `useView`, `useListView`, or `useLiveListView` decides which `Suspense` boundary handles the loading state.
625
+
626
+ ```tsx
627
+ import { Suspense } from 'react';
628
+ import { defer, useListView, useView, view, Deferred, ViewRef } from 'react-fate';
629
+
630
+ const CommentView = view<Comment>()({
631
+ content: true,
632
+ id: true,
633
+ });
634
+
635
+ const CommentConnectionView = {
636
+ args: { first: 3 },
637
+ items: { node: CommentView },
638
+ };
639
+
640
+ const PostView = view<Post>()({
641
+ comments: defer(CommentConnectionView),
642
+ content: true,
643
+ id: true,
644
+ title: true,
645
+ });
646
+
647
+ function PostCard({ post: postRef }: { post: ViewRef<'Post'> }) {
648
+ const post = useView(PostView, postRef);
649
+
650
+ return (
651
+ <article>
652
+ <h2>{post.title}</h2>
653
+ <p>{post.content}</p>
654
+ <Suspense fallback={<CommentsSkeleton />}>
655
+ <PostComments comments={post.comments} />
656
+ </Suspense>
657
+ </article>
658
+ );
659
+ }
660
+
661
+ function PostComments({
662
+ comments,
663
+ }: {
664
+ comments: Deferred<{ items: ReadonlyArray<{ node: ViewRef<'Comment'> }> }>;
665
+ }) {
666
+ const [items, loadNext] = useListView(CommentConnectionView, comments);
667
+
668
+ return (
669
+ <section>
670
+ {items.map(({ node }) => (
671
+ <CommentCard comment={node} key={node.id} />
672
+ ))}
673
+ {loadNext ? <button onClick={loadNext}>Load more</button> : null}
674
+ </section>
675
+ );
676
+ }
677
+ ```
678
+
679
+ Deferred fields are not optional data. They are explicit handles that existing view APIs can read. If the deferred selection is missing from the normalized cache, fate fetches only that missing selection and suspends the component that tried to resolve it.
680
+
681
+ This keeps parent components simple: eager fields like `title` and `content` are available when `useView(PostView, postRef)` returns, while slower or secondary fields such as `comments` can load under their own boundary.
682
+
683
+ GraphQL transports use the same client semantics today. The deferred field is omitted from the eager request and fetched when the deferred handle is resolved. GraphQL `@defer` is the natural transport representation for this feature, but consuming incremental multipart patches requires additional transport support before fate can safely normalize streamed patches from a single GraphQL response.
684
+
552
685
  ## List Views
553
686
 
554
687
  ### Pagination with `useListView`
@@ -904,9 +1037,9 @@ fate does not provide hooks for mutations like traditional data fetching librari
904
1037
 
905
1038
  Server mutations are exposed automatically as actions and mutations by fate's Vite plugin. The transport determines where those mutations are declared:
906
1039
 
907
- - With the [native HTTP transport](/docs/guide/server-integration.md#native-fate-protocol), mutations come from the `mutations` object passed to `createFateServer`.
908
- - With the [tRPC adapter](/docs/guide/server-integration.md#trpc-fate-setup), mutations come from tRPC mutation procedures exposed through your fate-enabled router.
909
- - With [Void](/docs/guide/void-integration.md), mutations use the same native fate server shape and are exposed through the Void route helpers.
1040
+ - With the [native HTTP transport](/docs/integrations/server.md#native-fate-protocol), mutations come from the `mutations` object passed to `createFateServer`.
1041
+ - With the [tRPC adapter](/docs/integrations/server.md#trpc-fate-setup), mutations come from tRPC mutation procedures exposed through your fate-enabled router.
1042
+ - With [Void](/docs/integrations/void.md), mutations use the same native fate server shape and are exposed through the Void route helpers.
910
1043
 
911
1044
  If you have a mutation named `post.like`, a `LikeButton` component using fate Actions and an async component library could look like this:
912
1045
 
@@ -1045,9 +1178,9 @@ You can call mutations from anywhere, and without waiting for previous mutations
1045
1178
 
1046
1179
  fate Actions & Mutations are backed by regular server mutations. If you already know how your fate server is wired, the client-side API above is the same regardless of transport. If not, start with the server setup for your environment:
1047
1180
 
1048
- - [Native HTTP custom mutations](/docs/guide/server-integration.md#custom-mutations) use `createFateServer({ mutations })`.
1049
- - [tRPC fate setup](/docs/guide/server-integration.md#trpc-fate-setup) wires fate into your tRPC router; custom writes can use the same `fate.createPlan` and `fate.resolveById` helpers shown there.
1050
- - [Void integration](/docs/guide/void-integration.md) exposes a native fate server from Void routes; define mutations with the native `createFateServer({ mutations })` API and serve them through `defineVoidFateRoute`.
1181
+ - [Native HTTP custom mutations](/docs/integrations/server.md#custom-mutations) use `createFateServer({ mutations })`.
1182
+ - [tRPC fate setup](/docs/integrations/server.md#trpc-fate-setup) wires fate into your tRPC router; custom writes can use the same `fate.createPlan` and `fate.resolveById` helpers shown there.
1183
+ - [Void integration](/docs/integrations/void.md) exposes a native fate server from Void routes; define mutations with the native `createFateServer({ mutations })` API and serve them through `defineVoidFateRoute`.
1051
1184
 
1052
1185
  Here is a native HTTP mutation for `post.like`:
1053
1186
 
@@ -1120,7 +1253,7 @@ export const postRouter = router({
1120
1253
  });
1121
1254
  ```
1122
1255
 
1123
- See [Server Integration](/docs/guide/server-integration.md) for complete native HTTP and tRPC setup examples, and [Void Integration](/docs/guide/void-integration.md) for route helpers when your app runs on Void.
1256
+ See [Server Integration](/docs/integrations/server.md) for complete native HTTP and tRPC setup examples, and [Void Integration](/docs/integrations/void.md) for route helpers when your app runs on Void.
1124
1257
 
1125
1258
  ### Action & Mutation Error Handling
1126
1259
 
@@ -1201,198 +1334,820 @@ addComment({
1201
1334
  });
1202
1335
  ```
1203
1336
 
1204
- ## Server Integration
1205
-
1206
- 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 wire the typed fate APIs into your app. _fate_ currently ships two server paths:
1337
+ ## Vue
1207
1338
 
1208
- - The native fate protocol, which is transport-agnostic and can be hosted by any Fetch-compatible server.
1209
- - The tRPC adapter, which keeps compatibility with existing tRPC backends.
1339
+ _fate_ also supports Vue through `vue-fate`. It exports the same core primitives as `react-fate` where Vue has a natural equivalent: `view`, `useRequest`, `useView`, `useListView`, `useLiveView`, `useLiveListView`, `useFateClient`, and the `FateClient` provider.
1210
1340
 
1211
- _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.
1341
+ Vue components use fate through Vue resources built from refs, computed values, watchers, and `<Suspense>`. The view model, generated client, normalized cache, masking, request shapes, list views, live views, and mutations are shared with the React adapter.
1212
1342
 
1213
- ### Conventions & Object Identity
1343
+ ### Installation
1214
1344
 
1215
- fate expects that data is served by a backend that follows these conventions:
1345
+ Install `vue-fate` in your Vue client:
1216
1346
 
1217
- - A `byId` query for each data type to fetch individual objects by their unique identifier (`id`).
1218
- - A `list` query for fetching lists of objects with support for pagination.
1347
+ ::: code-group
1219
1348
 
1220
- 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.
1349
+ ```bash [npm]
1350
+ npm add vue-fate
1351
+ ```
1221
1352
 
1222
- fate's type definitions might seem verbose at first glance. However, with fate's minimal API surface, AI tools can easily write this code for you, and the Vite plugin takes care of connecting it to your app.
1353
+ ```bash [pnpm]
1354
+ pnpm add vue-fate
1355
+ ```
1223
1356
 
1224
- > [!NOTE]
1225
- > You can adopt _fate_ incrementally in an existing tRPC codebase without changing your existing schema by adding these queries alongside your existing procedures.
1357
+ ```bash [yarn]
1358
+ yarn add vue-fate
1359
+ ```
1226
1360
 
1227
- ### Data Views
1361
+ :::
1228
1362
 
1229
- 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`.
1363
+ If your server lives in a separate package, install `@nkzw/fate` there as a runtime dependency too.
1230
1364
 
1231
- 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:
1365
+ ### Vite Plugin
1232
1366
 
1233
- ::: code-group
1367
+ Use the Vue adapter's Vite plugin in the client app:
1234
1368
 
1235
- ```tsx [Prisma]
1236
- import { dataView, type Entity } from '@nkzw/fate/server';
1237
- import type { User as PrismaUser } from '../prisma/prisma-client/client.ts';
1369
+ ```ts
1370
+ import { fate } from 'vue-fate/vite';
1371
+ import { defineConfig } from 'vite';
1372
+ import vue from '@vitejs/plugin-vue';
1238
1373
 
1239
- export const userDataView = dataView<PrismaUser>('User')({
1240
- id: true,
1241
- name: true,
1242
- username: true,
1374
+ export default defineConfig({
1375
+ plugins: [
1376
+ vue(),
1377
+ fate({
1378
+ module: '@your-org/server/fate.ts',
1379
+ transport: 'native',
1380
+ }),
1381
+ ],
1243
1382
  });
1244
-
1245
- export type User = Entity<typeof userDataView, 'User'>;
1246
1383
  ```
1247
1384
 
1248
- ```tsx [Drizzle]
1249
- import { dataView, type Entity } from '@nkzw/fate/server';
1250
- import type { UserRow } from '../drizzle/schema.ts';
1385
+ The plugin generates `vue-fate/client`, which contains the typed `createFateClient` helper for your app. The `module` and `transport` options are the same options used by the React adapter.
1251
1386
 
1252
- export const userDataView = dataView<UserRow>('User')({
1253
- id: true,
1254
- name: true,
1255
- username: true,
1256
- });
1387
+ ### Providing the Client
1257
1388
 
1258
- export type User = Entity<typeof userDataView, 'User'>;
1259
- ```
1389
+ Create a client with `createFateClient` and provide it with `FateClient`:
1260
1390
 
1261
- :::
1391
+ ```vue
1392
+ <script setup lang="ts">
1393
+ import { computed, ref } from 'vue';
1394
+ import { FateClient } from 'vue-fate';
1395
+ import { createFateClient } from 'vue-fate/client';
1396
+ import AppRoutes from './AppRoutes.vue';
1262
1397
 
1263
- 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:
1398
+ const token = ref<string | null>(null);
1264
1399
 
1265
- ```tsx
1266
- const UserData = view<User>()({
1267
- // Type-error + ignored during runtime.
1268
- password: true,
1269
- });
1400
+ const fate = computed(() =>
1401
+ createFateClient({
1402
+ headers: () => ({
1403
+ authorization: token.value ? `Bearer ${token.value}` : '',
1404
+ }),
1405
+ url: '/fate',
1406
+ }),
1407
+ );
1408
+ </script>
1409
+
1410
+ <template>
1411
+ <FateClient :client="fate">
1412
+ <Suspense>
1413
+ <AppRoutes />
1414
+ </Suspense>
1415
+ </FateClient>
1416
+ </template>
1270
1417
  ```
1271
1418
 
1272
- ### Data View Composition
1419
+ The `client` prop accepts a plain client, a ref, a computed value, or a getter. Descendants always read the current client, so switching credentials, endpoints, or transports does not require remounting the provider.
1273
1420
 
1274
- Similar to client-side views, data views can be composed of other data views:
1421
+ You can also install the client as a Vue plugin:
1275
1422
 
1276
- ```tsx
1277
- export const postDataView = dataView<PostItem>('Post')({
1278
- author: userDataView,
1279
- content: true,
1280
- id: true,
1281
- title: true,
1282
- });
1423
+ ```ts
1424
+ import { createApp } from 'vue';
1425
+ import { createFatePlugin } from 'vue-fate';
1426
+ import { createFateClient } from 'vue-fate/client';
1427
+ import App from './App.vue';
1428
+
1429
+ const fate = createFateClient({ url: '/fate' });
1430
+
1431
+ createApp(App).use(createFatePlugin(fate)).mount('#app');
1283
1432
  ```
1284
1433
 
1285
- ### Data View Lists
1434
+ ### Defining Views
1286
1435
 
1287
- Use the `list` helper to define list fields:
1436
+ Views are plain TypeScript values and can live anywhere. In Vue apps, it is usually best to define shared views in `.ts` modules and import them from single-file components:
1288
1437
 
1289
- ```tsx
1290
- import { list } from '@nkzw/fate/server';
1438
+ ```ts
1439
+ import type { Post, User } from '@your-org/server/views';
1440
+ import { view } from 'vue-fate';
1291
1441
 
1292
- export const commentDataView = dataView<CommentItem>('Comment')({
1293
- content: true,
1442
+ export const UserView = view<User>()({
1294
1443
  id: true,
1444
+ name: true,
1445
+ username: true,
1295
1446
  });
1296
1447
 
1297
- export const postDataView = dataView<PostItem>('Post')({
1298
- author: userDataView,
1299
- comments: list(commentDataView, { orderBy: [{ createdAt: 'asc' }, { id: 'asc' }] }),
1448
+ export const PostView = view<Post>()({
1449
+ author: UserView,
1450
+ id: true,
1451
+ title: true,
1300
1452
  });
1301
1453
  ```
1302
1454
 
1303
- We can define extra root-level lists and queries by exporting a `Root` object from our `views.ts` file using the same view syntax as everywhere else:
1455
+ Vue can import values across components, but single-file components have one default component export. Keeping reusable views in `.ts` files avoids coupling your data model to component files and makes view composition straightforward.
1304
1456
 
1305
- ```tsx
1306
- export const Root = {
1307
- categories: list(categoryDataView, { orderBy: [{ createdAt: 'asc' }, { id: 'asc' }] }),
1308
- commentSearch: {
1309
- procedure: 'search',
1310
- view: list(commentDataView, { orderBy: [{ createdAt: 'desc' }, { id: 'desc' }] }),
1457
+ ### Requests
1458
+
1459
+ `useRequest` declares the data a route, page, or component tree needs. It returns a resource with `data`, `pending`, `error`, `ready`, `refresh`, and `dispose`:
1460
+
1461
+ ```vue
1462
+ <script setup lang="ts">
1463
+ import { useListView, useRequest } from 'vue-fate';
1464
+ import { PostCardView } from '../fateViews';
1465
+ import PostCard from '../ui/PostCard.vue';
1466
+
1467
+ const request = useRequest({
1468
+ posts: {
1469
+ args: { first: 20 },
1470
+ list: PostCardView,
1311
1471
  },
1312
- events: list(eventDataView, { orderBy: [{ startAt: 'asc' }, { id: 'asc' }] }),
1313
- posts: list(postDataView, { orderBy: { createdAt: 'desc', id: 'desc' } }),
1314
- viewer: userDataView,
1315
- };
1472
+ });
1473
+
1474
+ const { posts } = await request.ready();
1475
+ const [postItems, loadNext] = useListView(PostCardView, posts);
1476
+ </script>
1477
+
1478
+ <template>
1479
+ <PostCard v-for="{ node } in postItems" :key="node.id" :post="node" />
1480
+ <button v-if="loadNext" @click="loadNext()">Load more</button>
1481
+ </template>
1316
1482
  ```
1317
1483
 
1318
- Entries that wrap their view in `list(...)` are treated as list resolvers. In the native protocol, the root key is the operation name used by the 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.
1484
+ Awaiting `ready()` in `<script setup>` participates in Vue Suspense. If you do not await it, read `request.data.value`, `request.pending.value`, and `request.error.value` in script, or use the refs directly in templates.
1319
1485
 
1320
- 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.
1486
+ ### Views in Components
1321
1487
 
1322
- For the above `Root` definitions, you can make the following requests using `useRequest`:
1488
+ Use `useView` to read a `ViewRef` from the normalized cache and subscribe to updates for the selected fields:
1323
1489
 
1324
- ```tsx
1325
- const query = 'Apple';
1490
+ ```vue
1491
+ <script setup lang="ts">
1492
+ import type { ViewRef } from 'vue-fate';
1493
+ import { useView } from 'vue-fate';
1494
+ import { PostCardView, UserView } from '../fateViews';
1495
+ import UserCard from './UserCard.vue';
1326
1496
 
1327
- const { posts, categories, viewer } = useRequest({
1328
- // Explicit Root queries:
1329
- categories: { list: categoryView },
1330
- commentSearch: { args: { query }, list: commentView },
1331
- events: { list: eventView },
1332
- posts: { list: postView },
1333
- viewer: { view: userView },
1497
+ const props = defineProps<{
1498
+ post: ViewRef<'Post'>;
1499
+ }>();
1334
1500
 
1335
- // Queries by id, if those entities have a `byId` query defined:
1336
- post: { id: '12', view: postView },
1337
- comment: { ids: ['6', '7'], view: commentView },
1338
- });
1501
+ const post = useView(PostCardView, () => props.post);
1502
+ const author = useView(UserView, () => post.value?.author ?? null);
1503
+ </script>
1504
+
1505
+ <template>
1506
+ <article v-if="post">
1507
+ <h2>{{ post.title }}</h2>
1508
+ <UserCard v-if="author" :user="author" />
1509
+ </article>
1510
+ </template>
1339
1511
  ```
1340
1512
 
1341
- ### Native fate protocol
1513
+ Pass reactive props through a getter so fate tracks prop changes. In script, resources are refs and need `.value`. In templates, Vue unwraps them automatically.
1342
1514
 
1343
- 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.
1515
+ ### Lists and Live Views
1344
1516
 
1345
- ```tsx
1346
- import { createFateServer, createHonoFateHandler } from '@nkzw/fate/server';
1347
- import { createPrismaSourceAdapter } from '@nkzw/fate/server/prisma';
1348
- import { Hono } from 'hono';
1349
- import type { AppContext } from './context.ts';
1350
- import { prisma } from './prisma.ts';
1351
- import { Root, userDataView } from './views.ts';
1517
+ `useListView` subscribes to a connection returned from `useRequest` or from a nested view field:
1352
1518
 
1353
- export { Root } from './views.ts';
1519
+ ```ts
1520
+ const [comments, loadNextCommentPage] = useListView(CommentView, () => post.value?.comments);
1521
+ ```
1354
1522
 
1355
- const sources = createPrismaSourceAdapter<AppContext>({
1356
- prisma: (ctx) => ctx.prisma,
1357
- views: Root,
1358
- });
1523
+ `useLiveView` and `useLiveListView` have the same resource shape as `useView` and `useListView`, but they also subscribe to server-pushed updates when the selected transport supports live views:
1359
1524
 
1360
- export const fate = createFateServer({
1361
- context: async ({ adapterContext }) => ({
1362
- prisma,
1363
- request: adapterContext.req.raw,
1364
- sessionUser: await getSessionUser(adapterContext.req.raw),
1365
- }),
1366
- queries: {
1367
- viewer: {
1368
- resolve: ({ ctx, select }) =>
1369
- sources.resolveById({
1370
- ctx,
1371
- id: ctx.sessionUser.id,
1372
- input: { select },
1373
- view: userDataView,
1374
- }),
1375
- },
1376
- },
1377
- roots: Root,
1378
- sources,
1379
- });
1525
+ ```ts
1526
+ const post = useLiveView(PostCardView, () => props.post);
1527
+ const [comments] = useLiveListView(CommentView, () => post.value?.comments);
1528
+ ```
1380
1529
 
1381
- const app = new Hono();
1382
- const handler = createHonoFateHandler(fate);
1530
+ Manual cleanup works through `dispose()`:
1383
1531
 
1384
- app.post('/fate', handler);
1385
- app.post('/fate/live', handler);
1532
+ ```ts
1533
+ const post = useLiveView(PostCardView, () => props.post);
1534
+
1535
+ onBeforeUnmount(() => {
1536
+ post.dispose();
1537
+ });
1386
1538
  ```
1387
1539
 
1388
- Configure the Vite plugin with the native transport:
1540
+ Vue scope disposal also cleans up resources automatically.
1389
1541
 
1390
- ```tsx
1391
- import { fate } from 'react-fate/vite';
1392
- import { defineConfig } from 'vite';
1542
+ ### Mutations
1543
+
1544
+ Use `useFateClient` to access generated mutations:
1545
+
1546
+ ```vue
1547
+ <script setup lang="ts">
1548
+ import { ref } from 'vue';
1549
+ import { useFateClient } from 'vue-fate';
1550
+
1551
+ const props = defineProps<{
1552
+ post: { id: string; likes: number };
1553
+ }>();
1554
+
1555
+ const fate = useFateClient();
1556
+ const pending = ref(false);
1557
+ const error = ref<unknown>(null);
1558
+
1559
+ const like = async () => {
1560
+ pending.value = true;
1561
+ error.value = null;
1562
+
1563
+ try {
1564
+ await fate.mutations.post.like({
1565
+ input: { id: props.post.id },
1566
+ optimistic: { likes: props.post.likes + 1 },
1567
+ });
1568
+ } catch (caughtError) {
1569
+ error.value = caughtError;
1570
+ } finally {
1571
+ pending.value = false;
1572
+ }
1573
+ };
1574
+ </script>
1575
+
1576
+ <template>
1577
+ <button :disabled="pending" @click="like">Like</button>
1578
+ </template>
1579
+ ```
1580
+
1581
+ The mutation call shape is the same as `react-fate`: `input`, `optimistic`, `insert`, and `view` work the same way. Vue does not have React Actions or `useActionState`, so Vue components should model pending and error state with Vue refs or your application state library.
1582
+
1583
+ ### API Differences from React
1584
+
1585
+ The names intentionally mirror `react-fate` where Vue has an equivalent API. The main differences are Vue framework differences:
1586
+
1587
+ - `useRequest`, `useView`, and list hooks return Vue resources instead of throwing promises from render.
1588
+ - Async setup and `<Suspense>` replace React's async component model.
1589
+ - Mutations use `fate.mutations` directly; React-only `fate.actions` and `useActionState` patterns do not apply.
1590
+ - Shared views are best kept in `.ts` modules instead of exporting named views from component files.
1591
+
1592
+ The generated client, server integrations, cache behavior, masking, pagination, optimistic updates, and live transport behavior are shared across adapters.
1593
+
1594
+ ## GraphQL Integration
1595
+
1596
+ _fate_ can use an existing GraphQL API as its transport. This keeps the adapter APIs, view composition, normalized cache, masking, requests, list views, live views, and mutations the same while replacing the native or tRPC backend with GraphQL operations.
1597
+
1598
+ Use the GraphQL transport when your backend already exposes GraphQL and you want fate's client model without adding fate's native server protocol.
1599
+
1600
+ ### Template
1601
+
1602
+ Create a client for an existing GraphQL server with:
1603
+
1604
+ ```bash
1605
+ vp create fate my-app --template graphql-client
1606
+ ```
1607
+
1608
+ Create a full GraphQL + Prisma example app with:
1609
+
1610
+ ```bash
1611
+ vp create fate my-app --template graphql
1612
+ ```
1613
+
1614
+ The client-only template is the smallest reference for the integration. It contains a `src/fate/graphql.ts` file that maps your GraphQL schema to fate views and roots.
1615
+
1616
+ ### GraphQL Schema Shape
1617
+
1618
+ The GraphQL transport expects a schema with Relay-style object identity and pagination:
1619
+
1620
+ - Entity objects include `id` and `__typename`.
1621
+ - Object fetches go through a `nodes(ids:)` field.
1622
+ - List fields return Relay connections with `edges`, `cursor`, `node`, and `pageInfo`.
1623
+ - Root queries and mutations return the entity type selected by the fate view.
1624
+
1625
+ For example, a `Post` list can be exposed as a normal GraphQL connection:
1626
+
1627
+ ```graphql
1628
+ type Query {
1629
+ posts(first: Int, after: String): PostConnection!
1630
+ viewer: User
1631
+ nodes(ids: [ID!]!): [Node]!
1632
+ }
1633
+
1634
+ type PostConnection {
1635
+ edges: [PostEdge!]!
1636
+ pageInfo: PageInfo!
1637
+ }
1638
+ ```
1639
+
1640
+ If your schema uses different root field names, keep the fate names you want on the client and map them with `fateGraphQL.roots`.
1641
+
1642
+ ### Mapping Your Schema
1643
+
1644
+ Create a module that exports data views, `Root`, and an optional `fateGraphQL` config. The Vite plugin reads this module during development and build time, generates the client wiring, and leaves your runtime GraphQL server unchanged.
1645
+
1646
+ ```tsx
1647
+ import { graphqlMutation } from '@nkzw/fate';
1648
+ import { dataView, list, type Entity } from '@nkzw/fate/server';
1649
+
1650
+ type GraphQLUser = {
1651
+ id: string;
1652
+ name?: string | null;
1653
+ username?: string | null;
1654
+ };
1655
+
1656
+ type GraphQLPost = {
1657
+ author?: GraphQLUser | null;
1658
+ id: string;
1659
+ title: string;
1660
+ };
1661
+
1662
+ export const userDataView = dataView<GraphQLUser>('User')({
1663
+ id: true,
1664
+ name: true,
1665
+ username: true,
1666
+ });
1667
+
1668
+ export const postDataView = dataView<GraphQLPost>('Post')({
1669
+ author: userDataView,
1670
+ id: true,
1671
+ title: true,
1672
+ });
1673
+
1674
+ export type User = Entity<typeof userDataView, 'User'>;
1675
+ export type Post = Entity<
1676
+ typeof postDataView,
1677
+ 'Post',
1678
+ {
1679
+ author: User | null;
1680
+ }
1681
+ >;
1682
+
1683
+ export const Root = {
1684
+ posts: list(postDataView),
1685
+ viewer: userDataView,
1686
+ };
1687
+
1688
+ export const fateGraphQL = {
1689
+ roots: {
1690
+ posts: { field: 'posts' },
1691
+ viewer: { field: 'viewer' },
1692
+ },
1693
+ } as const;
1694
+ ```
1695
+
1696
+ The data views describe the fields client components are allowed to select. `Root` describes the root operations available to `useRequest`. `fateGraphQL.roots` maps those root names to actual GraphQL fields. If the GraphQL field has the same name as the fate root, the `field` entry can be omitted.
1697
+
1698
+ ### Vite Plugin
1699
+
1700
+ Configure the fate Vite plugin with the GraphQL transport and point it at the mapping module:
1701
+
1702
+ ::: code-group
1703
+
1704
+ ```tsx [React]
1705
+ import { fate } from 'react-fate/vite';
1706
+ import { defineConfig } from 'vite';
1707
+
1708
+ export default defineConfig({
1709
+ plugins: [
1710
+ fate({
1711
+ module: './src/fate/graphql.ts',
1712
+ transport: 'graphql',
1713
+ }),
1714
+ ],
1715
+ });
1716
+ ```
1717
+
1718
+ ```ts [Vue]
1719
+ import vue from '@vitejs/plugin-vue';
1720
+ import { fate } from 'vue-fate/vite';
1721
+ import { defineConfig } from 'vite';
1722
+
1723
+ export default defineConfig({
1724
+ plugins: [
1725
+ vue(),
1726
+ fate({
1727
+ module: './src/fate/graphql.ts',
1728
+ transport: 'graphql',
1729
+ }),
1730
+ ],
1731
+ });
1732
+ ```
1733
+
1734
+ :::
1735
+
1736
+ The plugin generates a typed `createFateClient` helper from your views, roots, and GraphQL mapping. It also watches the mapping module and the files it imports during development.
1737
+
1738
+ ### Creating a Client
1739
+
1740
+ Create the client with your GraphQL endpoint and provide it through the `FateClient` provider:
1741
+
1742
+ ::: code-group
1743
+
1744
+ ```tsx [React]
1745
+ import { FateClient } from 'react-fate';
1746
+ import { createFateClient } from 'react-fate/client';
1747
+
1748
+ const fate = createFateClient({
1749
+ headers: () => ({
1750
+ authorization: `Bearer ${token}`,
1751
+ }),
1752
+ url: 'https://api.example.com/graphql',
1753
+ });
1754
+
1755
+ export function App() {
1756
+ return <FateClient client={fate}>{/* Components go here */}</FateClient>;
1757
+ }
1758
+ ```
1759
+
1760
+ ```vue [Vue]
1761
+ <script setup lang="ts">
1762
+ import { FateClient } from 'vue-fate';
1763
+ import { createFateClient } from 'vue-fate/client';
1764
+ import AppRoutes from './AppRoutes.vue';
1765
+
1766
+ const fate = createFateClient({
1767
+ headers: () => ({
1768
+ authorization: `Bearer ${token}`,
1769
+ }),
1770
+ url: 'https://api.example.com/graphql',
1771
+ });
1772
+ </script>
1773
+
1774
+ <template>
1775
+ <FateClient :client="fate">
1776
+ <AppRoutes />
1777
+ </FateClient>
1778
+ </template>
1779
+ ```
1780
+
1781
+ :::
1782
+
1783
+ Use `fetch` when you need to customize credentials or reuse an application fetch wrapper:
1784
+
1785
+ ```tsx
1786
+ const fate = createFateClient({
1787
+ fetch: (input, init) =>
1788
+ fetch(input, {
1789
+ ...init,
1790
+ credentials: 'include',
1791
+ }),
1792
+ url: `${env('SERVER_URL')}/graphql`,
1793
+ });
1794
+ ```
1795
+
1796
+ GraphQL operations issued in the same microtask are batched into a single GraphQL query or mutation document with aliased fields.
1797
+
1798
+ Deferred view fields work with the GraphQL transport through the same normalized cache flow as native HTTP: the eager query omits `defer(...)` fields, and `useView`, `useListView`, or `useLiveListView` fetches the missing selection through `nodes(ids:)` when the deferred handle is read. GraphQL `@defer` is the natural wire format for this feature, but fate's GraphQL transport currently expects one JSON result per operation and does not consume incremental multipart patches yet.
1799
+
1800
+ ### Object IDs
1801
+
1802
+ The transport converts between fate entity IDs and GraphQL node IDs. By default, it sends IDs as `${type}-${id}` and strips that prefix from returned IDs. Override this if your schema uses Relay global IDs, raw database IDs, or another encoding:
1803
+
1804
+ ```tsx
1805
+ const fate = createFateClient({
1806
+ decodeNodeId: (type, id) => {
1807
+ const [nodeType, nodeId] = atob(String(id)).split(':');
1808
+ if (nodeType !== type) {
1809
+ throw new Error(`Expected a ${type} node id.`);
1810
+ }
1811
+ return nodeId;
1812
+ },
1813
+ encodeNodeId: (type, id) => btoa(`${type}:${id}`),
1814
+ url: '/graphql',
1815
+ });
1816
+ ```
1817
+
1818
+ If your GraphQL API already accepts and returns the same IDs you use in the app, return `id` from both functions.
1819
+
1820
+ ### Requests and Arguments
1821
+
1822
+ Client code keeps using `useRequest` with the same shape as the other transports:
1823
+
1824
+ ```tsx
1825
+ const { posts, viewer } = useRequest({
1826
+ posts: {
1827
+ args: { first: 10 },
1828
+ list: PostView,
1829
+ },
1830
+ viewer: { view: UserView },
1831
+ });
1832
+ ```
1833
+
1834
+ Root arguments are sent to the root GraphQL field. Nested relation arguments are scoped by relation name:
1835
+
1836
+ ```tsx
1837
+ const { posts } = useRequest({
1838
+ posts: {
1839
+ args: {
1840
+ comments: { first: 3 },
1841
+ first: 10,
1842
+ },
1843
+ list: PostWithCommentsView,
1844
+ },
1845
+ });
1846
+ ```
1847
+
1848
+ This produces a root `posts(first: 10)` field and a nested `comments(first: 3)` field in the generated GraphQL selection.
1849
+
1850
+ ### Mutations
1851
+
1852
+ Map fate mutation names to GraphQL mutation fields with `graphqlMutation`:
1853
+
1854
+ ```tsx
1855
+ export const fateGraphQL = {
1856
+ mutations: {
1857
+ 'post.like': graphqlMutation<Post, { id: string }, Post>('Post', {
1858
+ field: 'postLike',
1859
+ }),
1860
+ },
1861
+ roots: {
1862
+ posts: { field: 'posts' },
1863
+ },
1864
+ } as const;
1865
+ ```
1866
+
1867
+ By default, the input is sent as an `input` argument:
1868
+
1869
+ ```graphql
1870
+ mutation {
1871
+ postLike(input: { id: "12" }) {
1872
+ id
1873
+ likes
1874
+ }
1875
+ }
1876
+ ```
1877
+
1878
+ Use `inputArg` when your schema uses a different argument name, or `inputArg: false` when the input object should be spread into field arguments:
1879
+
1880
+ ```tsx
1881
+ export const fateGraphQL = {
1882
+ mutations: {
1883
+ 'post.like': graphqlMutation<Post, { id: string }, Post>('Post', {
1884
+ field: 'likePost',
1885
+ inputArg: 'payload',
1886
+ }),
1887
+ 'user.follow': graphqlMutation<User, { id: string }, User>('User', {
1888
+ field: 'followUser',
1889
+ inputArg: false,
1890
+ }),
1891
+ },
1892
+ } as const;
1893
+ ```
1894
+
1895
+ Mutations use the same `mutation(...)` API described in the [Actions Guide](/docs/guide/actions.md). React clients can also expose those mutations as Actions for `useActionState`.
1896
+
1897
+ ### Live Views
1898
+
1899
+ GraphQL live views use [GraphQL SSE](https://github.com/enisdenjo/graphql-sse). Install `graphql-sse` in the client package and leave `live` enabled, or pass `live: false` when your schema does not support subscriptions.
1900
+
1901
+ ```tsx
1902
+ const fate = createFateClient({
1903
+ live: {
1904
+ url: 'https://api.example.com/graphql/stream',
1905
+ },
1906
+ url: 'https://api.example.com/graphql',
1907
+ });
1908
+ ```
1909
+
1910
+ The default subscription fields are `fateLiveNode` for `useLiveView` and `fateLiveConnection` for `useLiveListView`. Rename them with `entityField` and `connectionField`:
1911
+
1912
+ ```tsx
1913
+ const fate = createFateClient({
1914
+ live: {
1915
+ connectionField: 'liveConnection',
1916
+ entityField: 'liveNode',
1917
+ url: '/graphql/stream',
1918
+ },
1919
+ url: '/graphql',
1920
+ });
1921
+ ```
1922
+
1923
+ The live node subscription returns `{ data, delete, id, select }`. The live connection subscription returns events such as `appendNode`, `prependNode`, `deleteEdge`, and `invalidate`. These payloads match fate's live transport events, so the cache update behavior is the same as the native transport.
1924
+
1925
+ If you do not need live views, disable them explicitly:
1926
+
1927
+ ```tsx
1928
+ const fate = createFateClient({
1929
+ live: false,
1930
+ url: '/graphql',
1931
+ });
1932
+ ```
1933
+
1934
+ ### Existing Servers
1935
+
1936
+ The GraphQL transport is intentionally a mapping layer. It does not require `createFateServer`, the Prisma adapter, or the Drizzle adapter. Your GraphQL server remains responsible for authorization, validation, resolver behavior, cursor pagination, and mutation side effects.
1937
+
1938
+ Use data views to expose only the fields the client should be able to select, keep GraphQL schema authorization in your server, and treat `src/fate/graphql.ts` as the contract between your GraphQL API and fate's client.
1939
+
1940
+ ## Server Integration
1941
+
1942
+ Until now, we have focused on the client-side API of fate. You'll need a backend that can be wired into fate's typed request model so the Vite plugin can connect the typed fate APIs to your app. _fate_ currently ships three integration paths:
1943
+
1944
+ - The native fate protocol, which is transport-agnostic and can be hosted by any Fetch-compatible server.
1945
+ - The tRPC adapter, which keeps compatibility with existing tRPC backends.
1946
+ - The [GraphQL transport](/docs/integrations/graphql.md), which maps fate views and roots to an existing GraphQL schema.
1947
+
1948
+ _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.
1949
+
1950
+ ### Conventions & Object Identity
1951
+
1952
+ fate expects that data is served by a backend that follows these conventions:
1953
+
1954
+ - A `byId` query for each data type to fetch individual objects by their unique identifier (`id`).
1955
+ - A `list` query for fetching lists of objects with support for pagination.
1956
+
1957
+ 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.
1958
+
1959
+ fate's type definitions might seem verbose at first glance. However, with fate's minimal API surface, AI tools can easily write this code for you, and the Vite plugin takes care of connecting it to your app.
1960
+
1961
+ > [!NOTE]
1962
+ > You can adopt _fate_ incrementally in an existing tRPC codebase without changing your existing schema by adding these queries alongside your existing procedures.
1963
+
1964
+ ### Data Views
1965
+
1966
+ 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`.
1967
+
1968
+ 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:
1969
+
1970
+ ::: code-group
1971
+
1972
+ ```tsx [Prisma]
1973
+ import { dataView, type Entity } from '@nkzw/fate/server';
1974
+ import type { User as PrismaUser } from '../prisma/prisma-client/client.ts';
1975
+
1976
+ export const userDataView = dataView<PrismaUser>('User')({
1977
+ id: true,
1978
+ name: true,
1979
+ username: true,
1980
+ });
1981
+
1982
+ export type User = Entity<typeof userDataView, 'User'>;
1983
+ ```
1984
+
1985
+ ```tsx [Drizzle]
1986
+ import { dataView, type Entity } from '@nkzw/fate/server';
1987
+ import type { UserRow } from '../drizzle/schema.ts';
1988
+
1989
+ export const userDataView = dataView<UserRow>('User')({
1990
+ id: true,
1991
+ name: true,
1992
+ username: true,
1993
+ });
1994
+
1995
+ export type User = Entity<typeof userDataView, 'User'>;
1996
+ ```
1997
+
1998
+ :::
1999
+
2000
+ 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:
2001
+
2002
+ ```tsx
2003
+ const UserData = view<User>()({
2004
+ // Type-error + ignored during runtime.
2005
+ password: true,
2006
+ });
2007
+ ```
2008
+
2009
+ ### Data View Composition
2010
+
2011
+ Similar to client-side views, data views can be composed of other data views:
2012
+
2013
+ ```tsx
2014
+ export const postDataView = dataView<PostItem>('Post')({
2015
+ author: userDataView,
2016
+ content: true,
2017
+ id: true,
2018
+ title: true,
2019
+ });
2020
+ ```
2021
+
2022
+ ### Data View Lists
2023
+
2024
+ Use the `list` helper to define list fields:
2025
+
2026
+ ```tsx
2027
+ import { list } from '@nkzw/fate/server';
2028
+
2029
+ export const commentDataView = dataView<CommentItem>('Comment')({
2030
+ content: true,
2031
+ id: true,
2032
+ });
2033
+
2034
+ export const postDataView = dataView<PostItem>('Post')({
2035
+ author: userDataView,
2036
+ comments: list(commentDataView, { orderBy: [{ createdAt: 'asc' }, { id: 'asc' }] }),
2037
+ });
2038
+ ```
2039
+
2040
+ We can define extra root-level lists and queries by exporting a `Root` object from our `views.ts` file using the same view syntax as everywhere else:
2041
+
2042
+ ```tsx
2043
+ export const Root = {
2044
+ categories: list(categoryDataView, { orderBy: [{ createdAt: 'asc' }, { id: 'asc' }] }),
2045
+ commentSearch: {
2046
+ procedure: 'search',
2047
+ view: list(commentDataView, { orderBy: [{ createdAt: 'desc' }, { id: 'desc' }] }),
2048
+ },
2049
+ events: list(eventDataView, { orderBy: [{ startAt: 'asc' }, { id: 'asc' }] }),
2050
+ posts: list(postDataView, { orderBy: { createdAt: 'desc', id: 'desc' } }),
2051
+ viewer: userDataView,
2052
+ };
2053
+ ```
2054
+
2055
+ Entries that wrap their view in `list(...)` are treated as list resolvers. In the native protocol, the root key is the operation name used by the 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.
2056
+
2057
+ 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.
2058
+
2059
+ For the above `Root` definitions, you can make the following requests using `useRequest`:
2060
+
2061
+ ```tsx
2062
+ const query = 'Apple';
2063
+
2064
+ const { posts, categories, viewer } = useRequest({
2065
+ // Explicit Root queries:
2066
+ categories: { list: categoryView },
2067
+ commentSearch: { args: { query }, list: commentView },
2068
+ events: { list: eventView },
2069
+ posts: { list: postView },
2070
+ viewer: { view: userView },
2071
+
2072
+ // Queries by id, if those entities have a `byId` query defined:
2073
+ post: { id: '12', view: postView },
2074
+ comment: { ids: ['6', '7'], view: commentView },
2075
+ });
2076
+ ```
2077
+
2078
+ ### Native fate protocol
2079
+
2080
+ 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.
2081
+
2082
+ ```tsx
2083
+ import { createFateServer, createHonoFateHandler } from '@nkzw/fate/server';
2084
+ import { createPrismaSourceAdapter } from '@nkzw/fate/server/prisma';
2085
+ import { Hono } from 'hono';
2086
+ import type { AppContext } from './context.ts';
2087
+ import { prisma } from './prisma.ts';
2088
+ import { Root, userDataView } from './views.ts';
2089
+
2090
+ export { Root } from './views.ts';
2091
+
2092
+ const sources = createPrismaSourceAdapter<AppContext>({
2093
+ prisma: (ctx) => ctx.prisma,
2094
+ views: Root,
2095
+ });
2096
+
2097
+ export const fate = createFateServer({
2098
+ context: async ({ adapterContext }) => ({
2099
+ prisma,
2100
+ request: adapterContext.req.raw,
2101
+ sessionUser: await getSessionUser(adapterContext.req.raw),
2102
+ }),
2103
+ queries: {
2104
+ viewer: {
2105
+ resolve: ({ ctx, select }) =>
2106
+ sources.resolveById({
2107
+ ctx,
2108
+ id: ctx.sessionUser.id,
2109
+ input: { select },
2110
+ view: userDataView,
2111
+ }),
2112
+ },
2113
+ },
2114
+ roots: Root,
2115
+ sources,
2116
+ });
2117
+
2118
+ const app = new Hono();
2119
+ const handler = createHonoFateHandler(fate);
2120
+
2121
+ app.post('/fate', handler);
2122
+ app.post('/fate/live', handler);
2123
+ ```
2124
+
2125
+ Configure the Vite plugin with the native transport:
2126
+
2127
+ ::: code-group
2128
+
2129
+ ```tsx [React]
2130
+ import { fate } from 'react-fate/vite';
2131
+ import { defineConfig } from 'vite';
2132
+
2133
+ export default defineConfig({
2134
+ plugins: [
2135
+ fate({
2136
+ module: '@your-org/server/fate.ts',
2137
+ transport: 'native',
2138
+ }),
2139
+ ],
2140
+ });
2141
+ ```
2142
+
2143
+ ```ts [Vue]
2144
+ import vue from '@vitejs/plugin-vue';
2145
+ import { fate } from 'vue-fate/vite';
2146
+ import { defineConfig } from 'vite';
1393
2147
 
1394
2148
  export default defineConfig({
1395
2149
  plugins: [
2150
+ vue(),
1396
2151
  fate({
1397
2152
  module: '@your-org/server/fate.ts',
1398
2153
  transport: 'native',
@@ -1401,9 +2156,13 @@ export default defineConfig({
1401
2156
  });
1402
2157
  ```
1403
2158
 
2159
+ :::
2160
+
1404
2161
  With the native transport, the Vite plugin handles the HTTP transport setup. If you need to create a client manually, use `createFateClient` with the same route:
1405
2162
 
1406
- ```tsx
2163
+ ::: code-group
2164
+
2165
+ ```tsx [React]
1407
2166
  import { createFateClient } from 'react-fate/client';
1408
2167
 
1409
2168
  const client = createFateClient({
@@ -1411,6 +2170,16 @@ const client = createFateClient({
1411
2170
  });
1412
2171
  ```
1413
2172
 
2173
+ ```ts [Vue]
2174
+ import { createFateClient } from 'vue-fate/client';
2175
+
2176
+ const client = createFateClient({
2177
+ url: '/fate',
2178
+ });
2179
+ ```
2180
+
2181
+ :::
2182
+
1414
2183
  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.
1415
2184
 
1416
2185
  #### Custom Queries
@@ -1787,7 +2556,9 @@ export * from './views.ts';
1787
2556
 
1788
2557
  Configure the fate Vite plugin with your server module:
1789
2558
 
1790
- ```tsx
2559
+ ::: code-group
2560
+
2561
+ ```tsx [React]
1791
2562
  import { fate } from 'react-fate/vite';
1792
2563
  import { defineConfig } from 'vite';
1793
2564
 
@@ -1800,11 +2571,28 @@ export default defineConfig({
1800
2571
  });
1801
2572
  ```
1802
2573
 
2574
+ ```ts [Vue]
2575
+ import vue from '@vitejs/plugin-vue';
2576
+ import { fate } from 'vue-fate/vite';
2577
+ import { defineConfig } from 'vite';
2578
+
2579
+ export default defineConfig({
2580
+ plugins: [
2581
+ vue(),
2582
+ fate({
2583
+ module: '@your-org/server/trpc/router.ts',
2584
+ }),
2585
+ ],
2586
+ });
2587
+ ```
2588
+
2589
+ :::
2590
+
1803
2591
  _Note: fate uses the specified server module name to find the server types it needs. Make sure that the module is available to the client package's Vite config._
1804
2592
 
1805
2593
  During development, the plugin watches the server module and the files it imports. When one of those files changes, fate updates the internal client wiring and invalidates `@nkzw/fate/client` in Vite's module graph.
1806
2594
 
1807
- For a barebones client without React, import the plugin from `@nkzw/fate/vite` and the client APIs from `@nkzw/fate/client`. The plugin wires the same server types for the selected import path.
2595
+ For a barebones client without a framework adapter, import the plugin from `@nkzw/fate/vite` and the client APIs from `@nkzw/fate/client`. The plugin wires the same server types for the selected import path.
1808
2596
 
1809
2597
  The plugin writes project-local types under `.fate/`. If your TypeScript config does not already include dot-directories, extend the generated config:
1810
2598
 
@@ -1816,9 +2604,11 @@ The plugin writes project-local types under `.fate/`. If your TypeScript config
1816
2604
 
1817
2605
  ### Creating a _fate_ Client
1818
2606
 
1819
- Now that the Vite plugin has connected the types, create a fate client instance and provide it to your React app with the `FateClient` context provider:
2607
+ Now that the Vite plugin has connected the types, create a fate client instance and provide it to your app with the `FateClient` provider:
1820
2608
 
1821
- ```tsx
2609
+ ::: code-group
2610
+
2611
+ ```tsx [React]
1822
2612
  import { httpBatchLink } from '@trpc/client';
1823
2613
  import { FateClient } from 'react-fate';
1824
2614
  import { createFateClient } from 'react-fate/client';
@@ -1844,8 +2634,291 @@ export function App() {
1844
2634
  }
1845
2635
  ```
1846
2636
 
2637
+ ```vue [Vue]
2638
+ <script setup lang="ts">
2639
+ import { httpBatchLink } from '@trpc/client';
2640
+ import { computed } from 'vue';
2641
+ import { FateClient } from 'vue-fate';
2642
+ import { createFateClient } from 'vue-fate/client';
2643
+ import AppRoutes from './AppRoutes.vue';
2644
+
2645
+ const fate = computed(() =>
2646
+ createFateClient({
2647
+ links: [
2648
+ httpBatchLink({
2649
+ fetch: (input, init) =>
2650
+ fetch(input, {
2651
+ ...init,
2652
+ credentials: 'include',
2653
+ }),
2654
+ url: `${env('SERVER_URL')}/trpc`,
2655
+ }),
2656
+ ],
2657
+ }),
2658
+ );
2659
+ </script>
2660
+
2661
+ <template>
2662
+ <FateClient :client="fate">
2663
+ <AppRoutes />
2664
+ </FateClient>
2665
+ </template>
2666
+ ```
2667
+
2668
+ :::
2669
+
1847
2670
  _And you are all set. Happy building!_
1848
2671
 
2672
+ ## Void Integration
2673
+
2674
+ `void-fate` is the first-class [Void](https://void.cloud) adapter for fate to ease integration with the Void SDK and for deploying to the Void platform.
2675
+
2676
+ Use this integration when your app runs on Void and you want the example app's
2677
+ setup without copying its adapter glue.
2678
+
2679
+ ### Install
2680
+
2681
+ ::: code-group
2682
+
2683
+ ```sh [React]
2684
+ pnpm add @nkzw/fate react-fate void-fate void @void/react
2685
+ ```
2686
+
2687
+ ```sh [Vue]
2688
+ pnpm add @nkzw/fate vue-fate void-fate void @void/vue
2689
+ ```
2690
+
2691
+ :::
2692
+
2693
+ ### Vite
2694
+
2695
+ Use the framework adapter's Vite plugin with the Void transport:
2696
+
2697
+ ::: code-group
2698
+
2699
+ ```tsx [React]
2700
+ import { voidReact } from '@void/react/plugin';
2701
+ import { fate } from 'react-fate/vite';
2702
+ import { defineConfig } from 'vite-plus';
2703
+ import { voidPlugin } from 'void';
2704
+
2705
+ export default defineConfig({
2706
+ plugins: [
2707
+ voidPlugin(),
2708
+ voidReact(),
2709
+ fate({
2710
+ module: './src/fate/server.ts',
2711
+ transport: 'void',
2712
+ }),
2713
+ ],
2714
+ });
2715
+ ```
2716
+
2717
+ ```ts [Vue]
2718
+ import { voidVue } from '@void/vue/plugin';
2719
+ import { fate } from 'vue-fate/vite';
2720
+ import { defineConfig } from 'vite-plus';
2721
+ import { voidPlugin } from 'void';
2722
+
2723
+ export default defineConfig({
2724
+ plugins: [
2725
+ voidPlugin(),
2726
+ voidVue(),
2727
+ fate({
2728
+ module: './src/fate/server.ts',
2729
+ transport: 'void',
2730
+ }),
2731
+ ],
2732
+ });
2733
+ ```
2734
+
2735
+ :::
2736
+
2737
+ The Void transport uses `/fate` for RPC requests and `/fate-live` for live
2738
+ updates by default. In SSR, it calls the exported fate server directly. In the
2739
+ browser, it uses fetch and the SSE live endpoint.
2740
+
2741
+ ### Server Setup
2742
+
2743
+ Create a Void live adapter with `createVoidFateLive`, pass its `live` event bus
2744
+ to `createFateServer`, and export the adapter next to your fate server.
2745
+
2746
+ ```tsx
2747
+ import { createFateServer } from '@nkzw/fate/server';
2748
+ import { createDrizzleSourceAdapter } from '@nkzw/fate/server/drizzle';
2749
+ import { createVoidFateLive } from 'void-fate/server';
2750
+ import { db } from 'void/db';
2751
+ import schema from '../db/schema.ts';
2752
+ import { createContext } from './context.ts';
2753
+ import { Root } from './views.ts';
2754
+
2755
+ const sources = createDrizzleSourceAdapter({
2756
+ db,
2757
+ schema,
2758
+ views: Root,
2759
+ });
2760
+
2761
+ export const fateLive = createVoidFateLive();
2762
+ export const { live } = fateLive;
2763
+
2764
+ export const fateServer = createFateServer({
2765
+ context: ({ request }) => createContext({ request }),
2766
+ live,
2767
+ roots: Root,
2768
+ sources,
2769
+ });
2770
+ ```
2771
+
2772
+ Your app can publish live updates through the normal fate live bus:
2773
+
2774
+ ```tsx
2775
+ live.update('Post', postId, { changed: ['likes'] });
2776
+ live.connection('Post.comments', { id: postId }).appendNode('Comment', commentId, {
2777
+ node: comment,
2778
+ });
2779
+ ```
2780
+
2781
+ `changed` is optional. Void still uses generic topic fanout, while fate uses the changed field paths to refetch or write only the selected fields affected by the event.
2782
+
2783
+ ### Routes
2784
+
2785
+ Add one route for fate RPC requests:
2786
+
2787
+ ```tsx
2788
+ // routes/fate.ts
2789
+ import { defineVoidFateRoute } from 'void-fate/server';
2790
+ import { fateLive, fateServer } from '../src/fate/server.ts';
2791
+
2792
+ export const { GET, POST } = defineVoidFateRoute(fateServer, fateLive);
2793
+ ```
2794
+
2795
+ Add a second route for the live SSE transport:
2796
+
2797
+ ```tsx
2798
+ // routes/fate-live.ts
2799
+ import { defineVoidFateLiveRoute } from 'void-fate/server';
2800
+ import { fateLive, fateServer } from '../src/fate/server.ts';
2801
+
2802
+ export const { GET, POST } = defineVoidFateLiveRoute(fateServer, fateLive);
2803
+ ```
2804
+
2805
+ The live route handles `GET /fate-live` SSE connections and `POST /fate-live`
2806
+ control messages. `void-fate` does not use WebSockets.
2807
+
2808
+ ### Layout
2809
+
2810
+ Wrap your app with the Void fate client for your framework. It creates and
2811
+ provides the fate client through the matching adapter.
2812
+
2813
+ ::: code-group
2814
+
2815
+ ```tsx [React]
2816
+ import { useShared } from '@void/react';
2817
+ import type { ReactNode } from 'react';
2818
+ import { VoidFateClient } from 'void-fate/react';
2819
+ import type { SharedData } from '../src/lib/shared.ts';
2820
+
2821
+ export default function Layout({ children }: { children: ReactNode }) {
2822
+ const shared = useShared<SharedData>();
2823
+ const userId = shared.auth.user?.id;
2824
+ const origin = typeof window === 'undefined' ? shared.origin : window.location.origin;
2825
+
2826
+ return (
2827
+ <VoidFateClient origin={origin} userId={userId}>
2828
+ {children}
2829
+ </VoidFateClient>
2830
+ );
2831
+ }
2832
+ ```
2833
+
2834
+ ```vue [Vue]
2835
+ <script setup lang="ts">
2836
+ import { useShared } from '@void/vue';
2837
+ import { computed } from 'vue';
2838
+ import { FateClient } from 'vue-fate';
2839
+ import { createFateClient } from 'vue-fate/client';
2840
+ import type { SharedData } from '../src/lib/shared.ts';
2841
+
2842
+ const shared = useShared<SharedData>();
2843
+
2844
+ const fate = computed(() =>
2845
+ createFateClient({
2846
+ origin: typeof window === 'undefined' ? shared.origin : window.location.origin,
2847
+ userId: shared.auth.user?.id,
2848
+ }),
2849
+ );
2850
+ </script>
2851
+
2852
+ <template>
2853
+ <FateClient :client="fate">
2854
+ <slot />
2855
+ </FateClient>
2856
+ </template>
2857
+ ```
2858
+
2859
+ :::
2860
+
2861
+ `userId` is optional, but passing it lets the client be recreated when the
2862
+ signed-in user changes. Browser requests include credentials when a `userId` is
2863
+ present.
2864
+
2865
+ ### Custom Paths
2866
+
2867
+ The default route pair is `/fate` and `/fate-live`. If your Void app uses
2868
+ different paths, configure the same values on the live adapter and client.
2869
+
2870
+ ```tsx
2871
+ export const fateLive = createVoidFateLive({
2872
+ livePath: '/custom-fate-live',
2873
+ });
2874
+ ```
2875
+
2876
+ ::: code-group
2877
+
2878
+ ```tsx [React]
2879
+ <VoidFateClient livePath="/custom-fate-live" origin={origin} rpcPath="/custom-fate" userId={userId}>
2880
+ {children}
2881
+ </VoidFateClient>
2882
+ ```
2883
+
2884
+ ```vue [Vue]
2885
+ <script setup lang="ts">
2886
+ const fate = computed(() =>
2887
+ createFateClient({
2888
+ livePath: '/custom-fate-live',
2889
+ origin,
2890
+ rpcPath: '/custom-fate',
2891
+ userId,
2892
+ }),
2893
+ );
2894
+ </script>
2895
+
2896
+ <template>
2897
+ <FateClient :client="fate">
2898
+ <slot />
2899
+ </FateClient>
2900
+ </template>
2901
+ ```
2902
+
2903
+ :::
2904
+
2905
+ The route helper does not own the route path. Make sure your Void route filename
2906
+ or router configuration matches the paths you pass to the client.
2907
+
2908
+ ### Live Transport
2909
+
2910
+ Void can run separate request handlers for mutations and long-lived SSE
2911
+ connections. `createVoidFateLive` bridges those handlers by publishing live
2912
+ events from the request that changed data to the live route.
2913
+
2914
+ In local development, `void-fate` uses a development token for that internal
2915
+ publish request. Outside local development, Void must provide `__VOID_PROXY_TOKEN`
2916
+ in the route environment. If no internal publish token is available, the adapter
2917
+ falls back to the in-memory live bus for the current request context.
2918
+
2919
+ The live transport is best-effort and does not replay missed events after a
2920
+ client reconnects. This matches fate's default in-memory live event bus.
2921
+
1849
2922
  ## Frequently Asked Questions
1850
2923
 
1851
2924
  ### Is this serious software?