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
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **Pagination** = `object`
4
4
 
5
- Defined in: packages/fate/lib/transport-BTgtSwOM.d.mts:1086
5
+ Defined in: packages/fate/lib/transport-BjGm\_\_3t.d.mts:1100
6
6
 
7
7
  Pagination state returned alongside connection lists.
8
8
 
@@ -12,7 +12,7 @@ Pagination state returned alongside connection lists.
12
12
 
13
13
  > **hasNext**: `boolean`
14
14
 
15
- Defined in: packages/fate/lib/transport-BTgtSwOM.d.mts:1087
15
+ Defined in: packages/fate/lib/transport-BjGm\_\_3t.d.mts:1101
16
16
 
17
17
  ***
18
18
 
@@ -20,7 +20,7 @@ Defined in: packages/fate/lib/transport-BTgtSwOM.d.mts:1087
20
20
 
21
21
  > **hasPrevious**: `boolean`
22
22
 
23
- Defined in: packages/fate/lib/transport-BTgtSwOM.d.mts:1088
23
+ Defined in: packages/fate/lib/transport-BjGm\_\_3t.d.mts:1102
24
24
 
25
25
  ***
26
26
 
@@ -28,7 +28,7 @@ Defined in: packages/fate/lib/transport-BTgtSwOM.d.mts:1088
28
28
 
29
29
  > `optional` **nextCursor?**: `string`
30
30
 
31
- Defined in: packages/fate/lib/transport-BTgtSwOM.d.mts:1089
31
+ Defined in: packages/fate/lib/transport-BjGm\_\_3t.d.mts:1103
32
32
 
33
33
  ***
34
34
 
@@ -36,4 +36,4 @@ Defined in: packages/fate/lib/transport-BTgtSwOM.d.mts:1089
36
36
 
37
37
  > `optional` **previousCursor?**: `string`
38
38
 
39
- Defined in: packages/fate/lib/transport-BTgtSwOM.d.mts:1090
39
+ Defined in: packages/fate/lib/transport-BjGm\_\_3t.d.mts:1104
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **ViewRef**\<`TName`\> = `Readonly`\<\{ `__typename`: `TName`; `[ViewsTag]`: `Set`\<`string`\>; `id`: `string` \| `number`; \}\>
4
4
 
5
- Defined in: packages/fate/lib/transport-BTgtSwOM.d.mts:1068
5
+ Defined in: packages/fate/lib/transport-BjGm\_\_3t.d.mts:1082
6
6
 
7
7
  Reference to a normalized entity instance that can be resolved against one or more view tags.
8
8
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  > `const` **toEntityId**: (`type`, `rawId`) => `EntityId`
4
4
 
5
- Defined in: packages/fate/lib/index.d.mts:76
5
+ Defined in: packages/fate/lib/index.d.mts:81
6
6
 
7
7
  Builds the canonical cache ID for an entity.
8
8
 
@@ -7,9 +7,9 @@ fate does not provide hooks for mutations like traditional data fetching librari
7
7
 
8
8
  Server mutations are exposed automatically as actions and mutations by fate's Vite plugin. The transport determines where those mutations are declared:
9
9
 
10
- - With the [native HTTP transport](server-integration.md#native-fate-protocol), mutations come from the `mutations` object passed to `createFateServer`.
11
- - With the [tRPC adapter](server-integration.md#trpc-fate-setup), mutations come from tRPC mutation procedures exposed through your fate-enabled router.
12
- - With [Void](void-integration.md), mutations use the same native fate server shape and are exposed through the Void route helpers.
10
+ - With the [native HTTP transport](../integrations/server.md#native-fate-protocol), mutations come from the `mutations` object passed to `createFateServer`.
11
+ - With the [tRPC adapter](../integrations/server.md#trpc-fate-setup), mutations come from tRPC mutation procedures exposed through your fate-enabled router.
12
+ - With [Void](../integrations/void.md), mutations use the same native fate server shape and are exposed through the Void route helpers.
13
13
 
14
14
  If you have a mutation named `post.like`, a `LikeButton` component using fate Actions and an async component library could look like this:
15
15
 
@@ -148,9 +148,9 @@ You can call mutations from anywhere, and without waiting for previous mutations
148
148
 
149
149
  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:
150
150
 
151
- - [Native HTTP custom mutations](server-integration.md#custom-mutations) use `createFateServer({ mutations })`.
152
- - [tRPC fate setup](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.
153
- - [Void integration](void-integration.md) exposes a native fate server from Void routes; define mutations with the native `createFateServer({ mutations })` API and serve them through `defineVoidFateRoute`.
151
+ - [Native HTTP custom mutations](../integrations/server.md#custom-mutations) use `createFateServer({ mutations })`.
152
+ - [tRPC fate setup](../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.
153
+ - [Void integration](../integrations/void.md) exposes a native fate server from Void routes; define mutations with the native `createFateServer({ mutations })` API and serve them through `defineVoidFateRoute`.
154
154
 
155
155
  Here is a native HTTP mutation for `post.like`:
156
156
 
@@ -223,7 +223,7 @@ export const postRouter = router({
223
223
  });
224
224
  ```
225
225
 
226
- See [Server Integration](server-integration.md) for complete native HTTP and tRPC setup examples, and [Void Integration](void-integration.md) for route helpers when your app runs on Void.
226
+ See [Server Integration](../integrations/server.md) for complete native HTTP and tRPC setup examples, and [Void Integration](../integrations/void.md) for route helpers when your app runs on Void.
227
227
 
228
228
  ## Action & Mutation Error Handling
229
229
 
@@ -0,0 +1,62 @@
1
+ # Deferred Views
2
+
3
+ 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.
4
+
5
+ ```tsx
6
+ import { Suspense } from 'react';
7
+ import { defer, useListView, useView, view, Deferred, ViewRef } from 'react-fate';
8
+
9
+ const CommentView = view<Comment>()({
10
+ content: true,
11
+ id: true,
12
+ });
13
+
14
+ const CommentConnectionView = {
15
+ args: { first: 3 },
16
+ items: { node: CommentView },
17
+ };
18
+
19
+ const PostView = view<Post>()({
20
+ comments: defer(CommentConnectionView),
21
+ content: true,
22
+ id: true,
23
+ title: true,
24
+ });
25
+
26
+ function PostCard({ post: postRef }: { post: ViewRef<'Post'> }) {
27
+ const post = useView(PostView, postRef);
28
+
29
+ return (
30
+ <article>
31
+ <h2>{post.title}</h2>
32
+ <p>{post.content}</p>
33
+ <Suspense fallback={<CommentsSkeleton />}>
34
+ <PostComments comments={post.comments} />
35
+ </Suspense>
36
+ </article>
37
+ );
38
+ }
39
+
40
+ function PostComments({
41
+ comments,
42
+ }: {
43
+ comments: Deferred<{ items: ReadonlyArray<{ node: ViewRef<'Comment'> }> }>;
44
+ }) {
45
+ const [items, loadNext] = useListView(CommentConnectionView, comments);
46
+
47
+ return (
48
+ <section>
49
+ {items.map(({ node }) => (
50
+ <CommentCard comment={node} key={node.id} />
51
+ ))}
52
+ {loadNext ? <button onClick={loadNext}>Load more</button> : null}
53
+ </section>
54
+ );
55
+ }
56
+ ```
57
+
58
+ 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.
59
+
60
+ 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.
61
+
62
+ 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.
@@ -8,11 +8,11 @@ Create a new fate app with Vite+:
8
8
  vp create fate my-app
9
9
  ```
10
10
 
11
- 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.
11
+ 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.
12
12
 
13
13
  ## Manual Installation
14
14
 
15
- **_fate_** requires React 19.2+. For a React client, install `react-fate`:
15
+ For a React client, install `react-fate`. It requires React 19.2+:
16
16
 
17
17
  ::: code-group
18
18
 
@@ -30,7 +30,25 @@ yarn add react-fate
30
30
 
31
31
  :::
32
32
 
33
- 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:
33
+ For a Vue client, install `vue-fate`:
34
+
35
+ ::: code-group
36
+
37
+ ```bash [npm]
38
+ npm add vue-fate
39
+ ```
40
+
41
+ ```bash [pnpm]
42
+ pnpm add vue-fate
43
+ ```
44
+
45
+ ```bash [yarn]
46
+ yarn add vue-fate
47
+ ```
48
+
49
+ :::
50
+
51
+ 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:
34
52
 
35
53
  ::: code-group
36
54
 
@@ -0,0 +1,256 @@
1
+ # Vue
2
+
3
+ _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.
4
+
5
+ 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.
6
+
7
+ ## Installation
8
+
9
+ Install `vue-fate` in your Vue client:
10
+
11
+ ::: code-group
12
+
13
+ ```bash [npm]
14
+ npm add vue-fate
15
+ ```
16
+
17
+ ```bash [pnpm]
18
+ pnpm add vue-fate
19
+ ```
20
+
21
+ ```bash [yarn]
22
+ yarn add vue-fate
23
+ ```
24
+
25
+ :::
26
+
27
+ If your server lives in a separate package, install `@nkzw/fate` there as a runtime dependency too.
28
+
29
+ ## Vite Plugin
30
+
31
+ Use the Vue adapter's Vite plugin in the client app:
32
+
33
+ ```ts
34
+ import { fate } from 'vue-fate/vite';
35
+ import { defineConfig } from 'vite';
36
+ import vue from '@vitejs/plugin-vue';
37
+
38
+ export default defineConfig({
39
+ plugins: [
40
+ vue(),
41
+ fate({
42
+ module: '@your-org/server/fate.ts',
43
+ transport: 'native',
44
+ }),
45
+ ],
46
+ });
47
+ ```
48
+
49
+ 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.
50
+
51
+ ## Providing the Client
52
+
53
+ Create a client with `createFateClient` and provide it with `FateClient`:
54
+
55
+ ```vue
56
+ <script setup lang="ts">
57
+ import { computed, ref } from 'vue';
58
+ import { FateClient } from 'vue-fate';
59
+ import { createFateClient } from 'vue-fate/client';
60
+ import AppRoutes from './AppRoutes.vue';
61
+
62
+ const token = ref<string | null>(null);
63
+
64
+ const fate = computed(() =>
65
+ createFateClient({
66
+ headers: () => ({
67
+ authorization: token.value ? `Bearer ${token.value}` : '',
68
+ }),
69
+ url: '/fate',
70
+ }),
71
+ );
72
+ </script>
73
+
74
+ <template>
75
+ <FateClient :client="fate">
76
+ <Suspense>
77
+ <AppRoutes />
78
+ </Suspense>
79
+ </FateClient>
80
+ </template>
81
+ ```
82
+
83
+ 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.
84
+
85
+ You can also install the client as a Vue plugin:
86
+
87
+ ```ts
88
+ import { createApp } from 'vue';
89
+ import { createFatePlugin } from 'vue-fate';
90
+ import { createFateClient } from 'vue-fate/client';
91
+ import App from './App.vue';
92
+
93
+ const fate = createFateClient({ url: '/fate' });
94
+
95
+ createApp(App).use(createFatePlugin(fate)).mount('#app');
96
+ ```
97
+
98
+ ## Defining Views
99
+
100
+ 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:
101
+
102
+ ```ts
103
+ import type { Post, User } from '@your-org/server/views';
104
+ import { view } from 'vue-fate';
105
+
106
+ export const UserView = view<User>()({
107
+ id: true,
108
+ name: true,
109
+ username: true,
110
+ });
111
+
112
+ export const PostView = view<Post>()({
113
+ author: UserView,
114
+ id: true,
115
+ title: true,
116
+ });
117
+ ```
118
+
119
+ 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.
120
+
121
+ ## Requests
122
+
123
+ `useRequest` declares the data a route, page, or component tree needs. It returns a resource with `data`, `pending`, `error`, `ready`, `refresh`, and `dispose`:
124
+
125
+ ```vue
126
+ <script setup lang="ts">
127
+ import { useListView, useRequest } from 'vue-fate';
128
+ import { PostCardView } from '../fateViews';
129
+ import PostCard from '../ui/PostCard.vue';
130
+
131
+ const request = useRequest({
132
+ posts: {
133
+ args: { first: 20 },
134
+ list: PostCardView,
135
+ },
136
+ });
137
+
138
+ const { posts } = await request.ready();
139
+ const [postItems, loadNext] = useListView(PostCardView, posts);
140
+ </script>
141
+
142
+ <template>
143
+ <PostCard v-for="{ node } in postItems" :key="node.id" :post="node" />
144
+ <button v-if="loadNext" @click="loadNext()">Load more</button>
145
+ </template>
146
+ ```
147
+
148
+ 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.
149
+
150
+ ## Views in Components
151
+
152
+ Use `useView` to read a `ViewRef` from the normalized cache and subscribe to updates for the selected fields:
153
+
154
+ ```vue
155
+ <script setup lang="ts">
156
+ import type { ViewRef } from 'vue-fate';
157
+ import { useView } from 'vue-fate';
158
+ import { PostCardView, UserView } from '../fateViews';
159
+ import UserCard from './UserCard.vue';
160
+
161
+ const props = defineProps<{
162
+ post: ViewRef<'Post'>;
163
+ }>();
164
+
165
+ const post = useView(PostCardView, () => props.post);
166
+ const author = useView(UserView, () => post.value?.author ?? null);
167
+ </script>
168
+
169
+ <template>
170
+ <article v-if="post">
171
+ <h2>{{ post.title }}</h2>
172
+ <UserCard v-if="author" :user="author" />
173
+ </article>
174
+ </template>
175
+ ```
176
+
177
+ 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.
178
+
179
+ ## Lists and Live Views
180
+
181
+ `useListView` subscribes to a connection returned from `useRequest` or from a nested view field:
182
+
183
+ ```ts
184
+ const [comments, loadNextCommentPage] = useListView(CommentView, () => post.value?.comments);
185
+ ```
186
+
187
+ `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:
188
+
189
+ ```ts
190
+ const post = useLiveView(PostCardView, () => props.post);
191
+ const [comments] = useLiveListView(CommentView, () => post.value?.comments);
192
+ ```
193
+
194
+ Manual cleanup works through `dispose()`:
195
+
196
+ ```ts
197
+ const post = useLiveView(PostCardView, () => props.post);
198
+
199
+ onBeforeUnmount(() => {
200
+ post.dispose();
201
+ });
202
+ ```
203
+
204
+ Vue scope disposal also cleans up resources automatically.
205
+
206
+ ## Mutations
207
+
208
+ Use `useFateClient` to access generated mutations:
209
+
210
+ ```vue
211
+ <script setup lang="ts">
212
+ import { ref } from 'vue';
213
+ import { useFateClient } from 'vue-fate';
214
+
215
+ const props = defineProps<{
216
+ post: { id: string; likes: number };
217
+ }>();
218
+
219
+ const fate = useFateClient();
220
+ const pending = ref(false);
221
+ const error = ref<unknown>(null);
222
+
223
+ const like = async () => {
224
+ pending.value = true;
225
+ error.value = null;
226
+
227
+ try {
228
+ await fate.mutations.post.like({
229
+ input: { id: props.post.id },
230
+ optimistic: { likes: props.post.likes + 1 },
231
+ });
232
+ } catch (caughtError) {
233
+ error.value = caughtError;
234
+ } finally {
235
+ pending.value = false;
236
+ }
237
+ };
238
+ </script>
239
+
240
+ <template>
241
+ <button :disabled="pending" @click="like">Like</button>
242
+ </template>
243
+ ```
244
+
245
+ 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.
246
+
247
+ ## API Differences from React
248
+
249
+ The names intentionally mirror `react-fate` where Vue has an equivalent API. The main differences are Vue framework differences:
250
+
251
+ - `useRequest`, `useView`, and list hooks return Vue resources instead of throwing promises from render.
252
+ - Async setup and `<Suspense>` replace React's async component model.
253
+ - Mutations use `fate.mutations` directly; React-only `fate.actions` and `useActionState` patterns do not apply.
254
+ - Shared views are best kept in `.ts` modules instead of exporting named views from component files.
255
+
256
+ The generated client, server integrations, cache behavior, masking, pagination, optimistic updates, and live transport behavior are shared across adapters.
package/docs/index.md CHANGED
@@ -1,4 +1,5 @@
1
1
  # react-fate Docs
2
2
 
3
3
  - [Guides](guide/getting-started.md)
4
+ - [Integrations](integrations/server.md)
4
5
  - [API Reference](api/index.md)
@@ -0,0 +1,143 @@
1
+ # Cloudflare Integration
2
+
3
+ `cf-fate` is the first-class Cloudflare Workers adapter for Fate native HTTP transport and live views.
4
+
5
+ Use it when your backend runs directly on Cloudflare Workers and you want fate live views without adopting the Void platform.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pnpm add @nkzw/fate react-fate cf-fate drizzle-orm
11
+ pnpm add -D wrangler
12
+ ```
13
+
14
+ ## Server Setup
15
+
16
+ Create a Cloudflare live stream and pass its Fate live facade to `createFateServer`.
17
+
18
+ ```ts
19
+ // src/fate/live.ts
20
+ import { defineCloudflareFateLiveStream } from 'cf-fate/server';
21
+
22
+ export const fateStream = defineCloudflareFateLiveStream({
23
+ allowAnonymousControl: true,
24
+ binding: 'FATE_LIVE',
25
+ id: 'fate',
26
+ });
27
+ ```
28
+
29
+ ```ts
30
+ // src/fate/server.ts
31
+ import { createFateServer } from '@nkzw/fate/server';
32
+ import { createCloudflareFateLive } from 'cf-fate/server';
33
+
34
+ export const fateLive = createCloudflareFateLive();
35
+ export const { live } = fateLive;
36
+
37
+ export const fateServer = createFateServer({
38
+ live,
39
+ // context,
40
+ // roots,
41
+ // sources,
42
+ });
43
+ ```
44
+
45
+ Publish from mutations through the normal Fate live bus:
46
+
47
+ ```ts
48
+ live.update('Post', postId, { changed: ['likes'] });
49
+ live.connection('Post.comments', { id: postId }).appendNode('Comment', commentId);
50
+ ```
51
+
52
+ ## Worker Routes
53
+
54
+ Expose one route for Fate RPC and one route for the SSE live stream.
55
+
56
+ ```ts
57
+ import {
58
+ createCloudflareFateLiveDurableObject,
59
+ defineCloudflareFateLiveRoute,
60
+ defineCloudflareFateRoute,
61
+ } from 'cf-fate/server';
62
+ import { fateStream } from './fate/live';
63
+ import { fateLive, fateServer } from './fate/server';
64
+
65
+ const fateRoute = defineCloudflareFateRoute(fateServer, fateLive, { stream: fateStream });
66
+ const fateLiveRoute = defineCloudflareFateLiveRoute(fateStream);
67
+
68
+ export const FateLiveDurableObject = createCloudflareFateLiveDurableObject({
69
+ binding: 'FATE_LIVE',
70
+ });
71
+
72
+ export default {
73
+ fetch(request, env, ctx) {
74
+ const url = new URL(request.url);
75
+ if (url.pathname === '/fate') {
76
+ return fateRoute.fetch(request, env, ctx);
77
+ }
78
+ if (url.pathname === '/fate-live') {
79
+ return fateLiveRoute.fetch(request, env, ctx);
80
+ }
81
+ return new Response('Not Found', { status: 404 });
82
+ },
83
+ };
84
+ ```
85
+
86
+ ## Wrangler
87
+
88
+ Add a Durable Object binding and migration. `cf-fate` uses `node:async_hooks`, so the Worker must enable Node compatibility.
89
+
90
+ ```jsonc
91
+ {
92
+ "compatibility_flags": ["nodejs_compat"],
93
+ "durable_objects": {
94
+ "bindings": [
95
+ {
96
+ "name": "FATE_LIVE",
97
+ "class_name": "FateLiveDurableObject",
98
+ },
99
+ ],
100
+ },
101
+ "migrations": [
102
+ {
103
+ "tag": "fate-live-v1",
104
+ "new_sqlite_classes": ["FateLiveDurableObject"],
105
+ },
106
+ ],
107
+ }
108
+ ```
109
+
110
+ ## Client
111
+
112
+ Use the Cloudflare transport in the Fate Vite plugin:
113
+
114
+ ```ts
115
+ import { fate } from 'react-fate/vite';
116
+
117
+ fate({
118
+ module: './src/fate/server.ts',
119
+ transport: 'cloudflare',
120
+ });
121
+ ```
122
+
123
+ Then point the generated client at the Worker endpoints:
124
+
125
+ ```tsx
126
+ import { FateClient } from 'react-fate';
127
+ import { createFateClient } from 'react-fate/client';
128
+
129
+ const fate = createFateClient({
130
+ liveUrl: 'http://localhost:8787/fate-live',
131
+ url: 'http://localhost:8787/fate',
132
+ });
133
+
134
+ export function App({ children }) {
135
+ return <FateClient client={fate}>{children}</FateClient>;
136
+ }
137
+ ```
138
+
139
+ ## Semantics
140
+
141
+ `cf-fate` uses one browser `EventSource` per Fate client and multiplexes entity and connection topics over that stream. Durable Objects keep connection and topic subscription state so later requests, mutations, scheduled handlers, and queue consumers can publish to already-connected clients.
142
+
143
+ Delivery is at-most-once. Events are ordered within one topic, but events are not durably replayed after a disconnect. Use authoritative refetching or application-owned replay storage if missed events must be recovered.