react-fate 1.1.0 → 1.3.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 +745 -24
  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 +1 -1
  5. package/docs/api/functions/createGraphQLTransport.md +1 -1
  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 +1 -1
  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 +2 -0
  19. package/docs/api/type-aliases/ConnectionRef.md +1 -1
  20. package/docs/api/type-aliases/Deferred.md +19 -0
  21. package/docs/api/type-aliases/FateDehydratedState.md +1 -1
  22. package/docs/api/type-aliases/GraphQLMutationDefinition.md +1 -1
  23. package/docs/api/type-aliases/GraphQLMutationInput.md +1 -1
  24. package/docs/api/type-aliases/GraphQLMutationMap.md +1 -1
  25. package/docs/api/type-aliases/GraphQLMutationOutput.md +1 -1
  26. package/docs/api/type-aliases/GraphQLTransportOptions.md +11 -11
  27. package/docs/api/type-aliases/HydrateOptions.md +1 -1
  28. package/docs/api/type-aliases/HydrationLimits.md +1 -1
  29. package/docs/api/type-aliases/InferFateAPI.md +1 -1
  30. package/docs/api/type-aliases/Pagination.md +5 -5
  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/vue.md +256 -0
  37. package/docs/index.md +1 -0
  38. package/docs/integrations/cloudflare.md +143 -0
  39. package/docs/{guide/graphql-integration.md → integrations/graphql.md} +53 -6
  40. package/docs/{guide/server-integration.md → integrations/server.md} +93 -7
  41. package/docs/{guide/void-integration.md → integrations/void.md} +94 -12
  42. package/lib/index.d.mts +17 -10
  43. package/lib/index.mjs +110 -16
  44. package/package.json +2 -2
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 Prisma, a GraphQL app with Prisma, or a fate client for an existing GraphQL server. 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
 
@@ -601,6 +619,69 @@ fate.hydrate(loaderData.fate, { merge: 'replace' });
601
619
 
602
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.
603
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
+
604
685
  ## List Views
605
686
 
606
687
  ### Pagination with `useListView`
@@ -956,9 +1037,9 @@ fate does not provide hooks for mutations like traditional data fetching librari
956
1037
 
957
1038
  Server mutations are exposed automatically as actions and mutations by fate's Vite plugin. The transport determines where those mutations are declared:
958
1039
 
959
- - With the [native HTTP transport](/docs/guide/server-integration.md#native-fate-protocol), mutations come from the `mutations` object passed to `createFateServer`.
960
- - With the [tRPC adapter](/docs/guide/server-integration.md#trpc-fate-setup), mutations come from tRPC mutation procedures exposed through your fate-enabled router.
961
- - 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.
962
1043
 
963
1044
  If you have a mutation named `post.like`, a `LikeButton` component using fate Actions and an async component library could look like this:
964
1045
 
@@ -1097,9 +1178,9 @@ You can call mutations from anywhere, and without waiting for previous mutations
1097
1178
 
1098
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:
1099
1180
 
1100
- - [Native HTTP custom mutations](/docs/guide/server-integration.md#custom-mutations) use `createFateServer({ mutations })`.
1101
- - [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.
1102
- - [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`.
1103
1184
 
1104
1185
  Here is a native HTTP mutation for `post.like`:
1105
1186
 
@@ -1172,7 +1253,7 @@ export const postRouter = router({
1172
1253
  });
1173
1254
  ```
1174
1255
 
1175
- 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.
1176
1257
 
1177
1258
  ### Action & Mutation Error Handling
1178
1259
 
@@ -1253,9 +1334,266 @@ addComment({
1253
1334
  });
1254
1335
  ```
1255
1336
 
1337
+ ## Vue
1338
+
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.
1340
+
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.
1342
+
1343
+ ### Installation
1344
+
1345
+ Install `vue-fate` in your Vue client:
1346
+
1347
+ ::: code-group
1348
+
1349
+ ```bash [npm]
1350
+ npm add vue-fate
1351
+ ```
1352
+
1353
+ ```bash [pnpm]
1354
+ pnpm add vue-fate
1355
+ ```
1356
+
1357
+ ```bash [yarn]
1358
+ yarn add vue-fate
1359
+ ```
1360
+
1361
+ :::
1362
+
1363
+ If your server lives in a separate package, install `@nkzw/fate` there as a runtime dependency too.
1364
+
1365
+ ### Vite Plugin
1366
+
1367
+ Use the Vue adapter's Vite plugin in the client app:
1368
+
1369
+ ```ts
1370
+ import { fate } from 'vue-fate/vite';
1371
+ import { defineConfig } from 'vite';
1372
+ import vue from '@vitejs/plugin-vue';
1373
+
1374
+ export default defineConfig({
1375
+ plugins: [
1376
+ vue(),
1377
+ fate({
1378
+ module: '@your-org/server/fate.ts',
1379
+ transport: 'native',
1380
+ }),
1381
+ ],
1382
+ });
1383
+ ```
1384
+
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.
1386
+
1387
+ ### Providing the Client
1388
+
1389
+ Create a client with `createFateClient` and provide it with `FateClient`:
1390
+
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';
1397
+
1398
+ const token = ref<string | null>(null);
1399
+
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>
1417
+ ```
1418
+
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.
1420
+
1421
+ You can also install the client as a Vue plugin:
1422
+
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');
1432
+ ```
1433
+
1434
+ ### Defining Views
1435
+
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:
1437
+
1438
+ ```ts
1439
+ import type { Post, User } from '@your-org/server/views';
1440
+ import { view } from 'vue-fate';
1441
+
1442
+ export const UserView = view<User>()({
1443
+ id: true,
1444
+ name: true,
1445
+ username: true,
1446
+ });
1447
+
1448
+ export const PostView = view<Post>()({
1449
+ author: UserView,
1450
+ id: true,
1451
+ title: true,
1452
+ });
1453
+ ```
1454
+
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.
1456
+
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,
1471
+ },
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>
1482
+ ```
1483
+
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.
1485
+
1486
+ ### Views in Components
1487
+
1488
+ Use `useView` to read a `ViewRef` from the normalized cache and subscribe to updates for the selected fields:
1489
+
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';
1496
+
1497
+ const props = defineProps<{
1498
+ post: ViewRef<'Post'>;
1499
+ }>();
1500
+
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>
1511
+ ```
1512
+
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.
1514
+
1515
+ ### Lists and Live Views
1516
+
1517
+ `useListView` subscribes to a connection returned from `useRequest` or from a nested view field:
1518
+
1519
+ ```ts
1520
+ const [comments, loadNextCommentPage] = useListView(CommentView, () => post.value?.comments);
1521
+ ```
1522
+
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:
1524
+
1525
+ ```ts
1526
+ const post = useLiveView(PostCardView, () => props.post);
1527
+ const [comments] = useLiveListView(CommentView, () => post.value?.comments);
1528
+ ```
1529
+
1530
+ Manual cleanup works through `dispose()`:
1531
+
1532
+ ```ts
1533
+ const post = useLiveView(PostCardView, () => props.post);
1534
+
1535
+ onBeforeUnmount(() => {
1536
+ post.dispose();
1537
+ });
1538
+ ```
1539
+
1540
+ Vue scope disposal also cleans up resources automatically.
1541
+
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
+
1256
1594
  ## GraphQL Integration
1257
1595
 
1258
- _fate_ can use an existing GraphQL API as its transport. This keeps the React APIs, view composition, normalized cache, masking, requests, list views, live views, and actions the same while replacing the native or tRPC backend with GraphQL operations.
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.
1259
1597
 
1260
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.
1261
1599
 
@@ -1355,13 +1693,15 @@ export const fateGraphQL = {
1355
1693
  } as const;
1356
1694
  ```
1357
1695
 
1358
- The data views describe the fields React 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.
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.
1359
1697
 
1360
1698
  ### Vite Plugin
1361
1699
 
1362
1700
  Configure the fate Vite plugin with the GraphQL transport and point it at the mapping module:
1363
1701
 
1364
- ```tsx
1702
+ ::: code-group
1703
+
1704
+ ```tsx [React]
1365
1705
  import { fate } from 'react-fate/vite';
1366
1706
  import { defineConfig } from 'vite';
1367
1707
 
@@ -1375,13 +1715,33 @@ export default defineConfig({
1375
1715
  });
1376
1716
  ```
1377
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
+
1378
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.
1379
1737
 
1380
1738
  ### Creating a Client
1381
1739
 
1382
1740
  Create the client with your GraphQL endpoint and provide it through the `FateClient` provider:
1383
1741
 
1384
- ```tsx
1742
+ ::: code-group
1743
+
1744
+ ```tsx [React]
1385
1745
  import { FateClient } from 'react-fate';
1386
1746
  import { createFateClient } from 'react-fate/client';
1387
1747
 
@@ -1397,6 +1757,29 @@ export function App() {
1397
1757
  }
1398
1758
  ```
1399
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
+
1400
1783
  Use `fetch` when you need to customize credentials or reuse an application fetch wrapper:
1401
1784
 
1402
1785
  ```tsx
@@ -1412,6 +1795,8 @@ const fate = createFateClient({
1412
1795
 
1413
1796
  GraphQL operations issued in the same microtask are batched into a single GraphQL query or mutation document with aliased fields.
1414
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
+
1415
1800
  ### Object IDs
1416
1801
 
1417
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:
@@ -1507,7 +1892,7 @@ export const fateGraphQL = {
1507
1892
  } as const;
1508
1893
  ```
1509
1894
 
1510
- Actions use the same `mutation(...)` and `useActionState` APIs described in the [Actions Guide](/docs/guide/actions.md).
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`.
1511
1896
 
1512
1897
  ### Live Views
1513
1898
 
@@ -1550,7 +1935,7 @@ const fate = createFateClient({
1550
1935
 
1551
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.
1552
1937
 
1553
- 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 React client.
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.
1554
1939
 
1555
1940
  ## Server Integration
1556
1941
 
@@ -1558,7 +1943,7 @@ Until now, we have focused on the client-side API of fate. You'll need a backend
1558
1943
 
1559
1944
  - The native fate protocol, which is transport-agnostic and can be hosted by any Fetch-compatible server.
1560
1945
  - The tRPC adapter, which keeps compatibility with existing tRPC backends.
1561
- - The [GraphQL transport](/docs/guide/graphql-integration.md), which maps fate views and roots to an existing GraphQL schema.
1946
+ - The [GraphQL transport](/docs/integrations/graphql.md), which maps fate views and roots to an existing GraphQL schema.
1562
1947
 
1563
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.
1564
1949
 
@@ -1739,7 +2124,9 @@ app.post('/fate/live', handler);
1739
2124
 
1740
2125
  Configure the Vite plugin with the native transport:
1741
2126
 
1742
- ```tsx
2127
+ ::: code-group
2128
+
2129
+ ```tsx [React]
1743
2130
  import { fate } from 'react-fate/vite';
1744
2131
  import { defineConfig } from 'vite';
1745
2132
 
@@ -1753,9 +2140,29 @@ export default defineConfig({
1753
2140
  });
1754
2141
  ```
1755
2142
 
2143
+ ```ts [Vue]
2144
+ import vue from '@vitejs/plugin-vue';
2145
+ import { fate } from 'vue-fate/vite';
2146
+ import { defineConfig } from 'vite';
2147
+
2148
+ export default defineConfig({
2149
+ plugins: [
2150
+ vue(),
2151
+ fate({
2152
+ module: '@your-org/server/fate.ts',
2153
+ transport: 'native',
2154
+ }),
2155
+ ],
2156
+ });
2157
+ ```
2158
+
2159
+ :::
2160
+
1756
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:
1757
2162
 
1758
- ```tsx
2163
+ ::: code-group
2164
+
2165
+ ```tsx [React]
1759
2166
  import { createFateClient } from 'react-fate/client';
1760
2167
 
1761
2168
  const client = createFateClient({
@@ -1763,6 +2170,16 @@ const client = createFateClient({
1763
2170
  });
1764
2171
  ```
1765
2172
 
2173
+ ```ts [Vue]
2174
+ import { createFateClient } from 'vue-fate/client';
2175
+
2176
+ const client = createFateClient({
2177
+ url: '/fate',
2178
+ });
2179
+ ```
2180
+
2181
+ :::
2182
+
1766
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.
1767
2184
 
1768
2185
  #### Custom Queries
@@ -2139,7 +2556,9 @@ export * from './views.ts';
2139
2556
 
2140
2557
  Configure the fate Vite plugin with your server module:
2141
2558
 
2142
- ```tsx
2559
+ ::: code-group
2560
+
2561
+ ```tsx [React]
2143
2562
  import { fate } from 'react-fate/vite';
2144
2563
  import { defineConfig } from 'vite';
2145
2564
 
@@ -2152,11 +2571,28 @@ export default defineConfig({
2152
2571
  });
2153
2572
  ```
2154
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
+
2155
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._
2156
2592
 
2157
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.
2158
2594
 
2159
- 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.
2160
2596
 
2161
2597
  The plugin writes project-local types under `.fate/`. If your TypeScript config does not already include dot-directories, extend the generated config:
2162
2598
 
@@ -2168,9 +2604,11 @@ The plugin writes project-local types under `.fate/`. If your TypeScript config
2168
2604
 
2169
2605
  ### Creating a _fate_ Client
2170
2606
 
2171
- 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:
2172
2608
 
2173
- ```tsx
2609
+ ::: code-group
2610
+
2611
+ ```tsx [React]
2174
2612
  import { httpBatchLink } from '@trpc/client';
2175
2613
  import { FateClient } from 'react-fate';
2176
2614
  import { createFateClient } from 'react-fate/client';
@@ -2196,8 +2634,291 @@ export function App() {
2196
2634
  }
2197
2635
  ```
2198
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
+
2199
2670
  _And you are all set. Happy building!_
2200
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
+
2201
2922
  ## Frequently Asked Questions
2202
2923
 
2203
2924
  ### Is this serious software?