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.
- package/README.md +745 -24
- package/docs/api/functions/FateClient.md +1 -1
- package/docs/api/functions/clientRoot.md +1 -1
- package/docs/api/functions/createClient.md +1 -1
- package/docs/api/functions/createGraphQLTransport.md +1 -1
- package/docs/api/functions/createHTTPTransport.md +1 -1
- package/docs/api/functions/createTRPCTransport.md +1 -1
- package/docs/api/functions/defer.md +21 -0
- package/docs/api/functions/graphqlMutation.md +1 -1
- package/docs/api/functions/mutation.md +1 -1
- package/docs/api/functions/useFateClient.md +1 -1
- package/docs/api/functions/useListView.md +4 -4
- package/docs/api/functions/useLiveListView.md +4 -4
- package/docs/api/functions/useLiveView.md +52 -11
- package/docs/api/functions/useRequest.md +1 -1
- package/docs/api/functions/useView.md +85 -11
- package/docs/api/functions/view.md +1 -1
- package/docs/api/index.md +2 -0
- package/docs/api/type-aliases/ConnectionRef.md +1 -1
- package/docs/api/type-aliases/Deferred.md +19 -0
- package/docs/api/type-aliases/FateDehydratedState.md +1 -1
- package/docs/api/type-aliases/GraphQLMutationDefinition.md +1 -1
- package/docs/api/type-aliases/GraphQLMutationInput.md +1 -1
- package/docs/api/type-aliases/GraphQLMutationMap.md +1 -1
- package/docs/api/type-aliases/GraphQLMutationOutput.md +1 -1
- package/docs/api/type-aliases/GraphQLTransportOptions.md +11 -11
- package/docs/api/type-aliases/HydrateOptions.md +1 -1
- package/docs/api/type-aliases/HydrationLimits.md +1 -1
- package/docs/api/type-aliases/InferFateAPI.md +1 -1
- package/docs/api/type-aliases/Pagination.md +5 -5
- package/docs/api/type-aliases/ViewRef.md +1 -1
- package/docs/api/variables/toEntityId.md +1 -1
- package/docs/guide/actions.md +7 -7
- package/docs/guide/deferred-views.md +62 -0
- package/docs/guide/getting-started.md +21 -3
- package/docs/guide/vue.md +256 -0
- package/docs/index.md +1 -0
- package/docs/integrations/cloudflare.md +143 -0
- package/docs/{guide/graphql-integration.md → integrations/graphql.md} +53 -6
- package/docs/{guide/server-integration.md → integrations/server.md} +93 -7
- package/docs/{guide/void-integration.md → integrations/void.md} +94 -12
- package/lib/index.d.mts +17 -10
- package/lib/index.mjs +110 -16
- 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
|
-
|
|
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
|
-
|
|
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/
|
|
960
|
-
- With the [tRPC adapter](/docs/
|
|
961
|
-
- With [Void](/docs/
|
|
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/
|
|
1101
|
-
- [tRPC fate setup](/docs/
|
|
1102
|
-
- [Void integration](/docs/
|
|
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/
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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?
|