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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GraphQL Integration
|
|
2
2
|
|
|
3
|
-
_fate_ can use an existing GraphQL API as its transport. This keeps the
|
|
3
|
+
_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.
|
|
4
4
|
|
|
5
5
|
Use the GraphQL transport when your backend already exposes GraphQL and you want fate's client model without adding fate's native server protocol.
|
|
6
6
|
|
|
@@ -100,13 +100,15 @@ export const fateGraphQL = {
|
|
|
100
100
|
} as const;
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
-
The data views describe the fields
|
|
103
|
+
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.
|
|
104
104
|
|
|
105
105
|
## Vite Plugin
|
|
106
106
|
|
|
107
107
|
Configure the fate Vite plugin with the GraphQL transport and point it at the mapping module:
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
::: code-group
|
|
110
|
+
|
|
111
|
+
```tsx [React]
|
|
110
112
|
import { fate } from 'react-fate/vite';
|
|
111
113
|
import { defineConfig } from 'vite';
|
|
112
114
|
|
|
@@ -120,13 +122,33 @@ export default defineConfig({
|
|
|
120
122
|
});
|
|
121
123
|
```
|
|
122
124
|
|
|
125
|
+
```ts [Vue]
|
|
126
|
+
import vue from '@vitejs/plugin-vue';
|
|
127
|
+
import { fate } from 'vue-fate/vite';
|
|
128
|
+
import { defineConfig } from 'vite';
|
|
129
|
+
|
|
130
|
+
export default defineConfig({
|
|
131
|
+
plugins: [
|
|
132
|
+
vue(),
|
|
133
|
+
fate({
|
|
134
|
+
module: './src/fate/graphql.ts',
|
|
135
|
+
transport: 'graphql',
|
|
136
|
+
}),
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
:::
|
|
142
|
+
|
|
123
143
|
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.
|
|
124
144
|
|
|
125
145
|
## Creating a Client
|
|
126
146
|
|
|
127
147
|
Create the client with your GraphQL endpoint and provide it through the `FateClient` provider:
|
|
128
148
|
|
|
129
|
-
|
|
149
|
+
::: code-group
|
|
150
|
+
|
|
151
|
+
```tsx [React]
|
|
130
152
|
import { FateClient } from 'react-fate';
|
|
131
153
|
import { createFateClient } from 'react-fate/client';
|
|
132
154
|
|
|
@@ -142,6 +164,29 @@ export function App() {
|
|
|
142
164
|
}
|
|
143
165
|
```
|
|
144
166
|
|
|
167
|
+
```vue [Vue]
|
|
168
|
+
<script setup lang="ts">
|
|
169
|
+
import { FateClient } from 'vue-fate';
|
|
170
|
+
import { createFateClient } from 'vue-fate/client';
|
|
171
|
+
import AppRoutes from './AppRoutes.vue';
|
|
172
|
+
|
|
173
|
+
const fate = createFateClient({
|
|
174
|
+
headers: () => ({
|
|
175
|
+
authorization: `Bearer ${token}`,
|
|
176
|
+
}),
|
|
177
|
+
url: 'https://api.example.com/graphql',
|
|
178
|
+
});
|
|
179
|
+
</script>
|
|
180
|
+
|
|
181
|
+
<template>
|
|
182
|
+
<FateClient :client="fate">
|
|
183
|
+
<AppRoutes />
|
|
184
|
+
</FateClient>
|
|
185
|
+
</template>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
:::
|
|
189
|
+
|
|
145
190
|
Use `fetch` when you need to customize credentials or reuse an application fetch wrapper:
|
|
146
191
|
|
|
147
192
|
```tsx
|
|
@@ -157,6 +202,8 @@ const fate = createFateClient({
|
|
|
157
202
|
|
|
158
203
|
GraphQL operations issued in the same microtask are batched into a single GraphQL query or mutation document with aliased fields.
|
|
159
204
|
|
|
205
|
+
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.
|
|
206
|
+
|
|
160
207
|
## Object IDs
|
|
161
208
|
|
|
162
209
|
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:
|
|
@@ -252,7 +299,7 @@ export const fateGraphQL = {
|
|
|
252
299
|
} as const;
|
|
253
300
|
```
|
|
254
301
|
|
|
255
|
-
|
|
302
|
+
Mutations use the same `mutation(...)` API described in the [Actions Guide](../guide/actions.md). React clients can also expose those mutations as Actions for `useActionState`.
|
|
256
303
|
|
|
257
304
|
## Live Views
|
|
258
305
|
|
|
@@ -295,4 +342,4 @@ const fate = createFateClient({
|
|
|
295
342
|
|
|
296
343
|
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.
|
|
297
344
|
|
|
298
|
-
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
|
|
345
|
+
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.
|
|
@@ -4,7 +4,7 @@ Until now, we have focused on the client-side API of fate. You'll need a backend
|
|
|
4
4
|
|
|
5
5
|
- The native fate protocol, which is transport-agnostic and can be hosted by any Fetch-compatible server.
|
|
6
6
|
- The tRPC adapter, which keeps compatibility with existing tRPC backends.
|
|
7
|
-
- The [GraphQL transport](graphql
|
|
7
|
+
- The [GraphQL transport](graphql.md), which maps fate views and roots to an existing GraphQL schema.
|
|
8
8
|
|
|
9
9
|
_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.
|
|
10
10
|
|
|
@@ -185,7 +185,9 @@ app.post('/fate/live', handler);
|
|
|
185
185
|
|
|
186
186
|
Configure the Vite plugin with the native transport:
|
|
187
187
|
|
|
188
|
-
|
|
188
|
+
::: code-group
|
|
189
|
+
|
|
190
|
+
```tsx [React]
|
|
189
191
|
import { fate } from 'react-fate/vite';
|
|
190
192
|
import { defineConfig } from 'vite';
|
|
191
193
|
|
|
@@ -199,9 +201,29 @@ export default defineConfig({
|
|
|
199
201
|
});
|
|
200
202
|
```
|
|
201
203
|
|
|
204
|
+
```ts [Vue]
|
|
205
|
+
import vue from '@vitejs/plugin-vue';
|
|
206
|
+
import { fate } from 'vue-fate/vite';
|
|
207
|
+
import { defineConfig } from 'vite';
|
|
208
|
+
|
|
209
|
+
export default defineConfig({
|
|
210
|
+
plugins: [
|
|
211
|
+
vue(),
|
|
212
|
+
fate({
|
|
213
|
+
module: '@your-org/server/fate.ts',
|
|
214
|
+
transport: 'native',
|
|
215
|
+
}),
|
|
216
|
+
],
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
:::
|
|
221
|
+
|
|
202
222
|
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:
|
|
203
223
|
|
|
204
|
-
|
|
224
|
+
::: code-group
|
|
225
|
+
|
|
226
|
+
```tsx [React]
|
|
205
227
|
import { createFateClient } from 'react-fate/client';
|
|
206
228
|
|
|
207
229
|
const client = createFateClient({
|
|
@@ -209,6 +231,16 @@ const client = createFateClient({
|
|
|
209
231
|
});
|
|
210
232
|
```
|
|
211
233
|
|
|
234
|
+
```ts [Vue]
|
|
235
|
+
import { createFateClient } from 'vue-fate/client';
|
|
236
|
+
|
|
237
|
+
const client = createFateClient({
|
|
238
|
+
url: '/fate',
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
:::
|
|
243
|
+
|
|
212
244
|
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.
|
|
213
245
|
|
|
214
246
|
### Custom Queries
|
|
@@ -585,7 +617,9 @@ export * from './views.ts';
|
|
|
585
617
|
|
|
586
618
|
Configure the fate Vite plugin with your server module:
|
|
587
619
|
|
|
588
|
-
|
|
620
|
+
::: code-group
|
|
621
|
+
|
|
622
|
+
```tsx [React]
|
|
589
623
|
import { fate } from 'react-fate/vite';
|
|
590
624
|
import { defineConfig } from 'vite';
|
|
591
625
|
|
|
@@ -598,11 +632,28 @@ export default defineConfig({
|
|
|
598
632
|
});
|
|
599
633
|
```
|
|
600
634
|
|
|
635
|
+
```ts [Vue]
|
|
636
|
+
import vue from '@vitejs/plugin-vue';
|
|
637
|
+
import { fate } from 'vue-fate/vite';
|
|
638
|
+
import { defineConfig } from 'vite';
|
|
639
|
+
|
|
640
|
+
export default defineConfig({
|
|
641
|
+
plugins: [
|
|
642
|
+
vue(),
|
|
643
|
+
fate({
|
|
644
|
+
module: '@your-org/server/trpc/router.ts',
|
|
645
|
+
}),
|
|
646
|
+
],
|
|
647
|
+
});
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
:::
|
|
651
|
+
|
|
601
652
|
_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._
|
|
602
653
|
|
|
603
654
|
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.
|
|
604
655
|
|
|
605
|
-
For a barebones client without
|
|
656
|
+
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.
|
|
606
657
|
|
|
607
658
|
The plugin writes project-local types under `.fate/`. If your TypeScript config does not already include dot-directories, extend the generated config:
|
|
608
659
|
|
|
@@ -614,9 +665,11 @@ The plugin writes project-local types under `.fate/`. If your TypeScript config
|
|
|
614
665
|
|
|
615
666
|
## Creating a _fate_ Client
|
|
616
667
|
|
|
617
|
-
Now that the Vite plugin has connected the types, create a fate client instance and provide it to your
|
|
668
|
+
Now that the Vite plugin has connected the types, create a fate client instance and provide it to your app with the `FateClient` provider:
|
|
618
669
|
|
|
619
|
-
|
|
670
|
+
::: code-group
|
|
671
|
+
|
|
672
|
+
```tsx [React]
|
|
620
673
|
import { httpBatchLink } from '@trpc/client';
|
|
621
674
|
import { FateClient } from 'react-fate';
|
|
622
675
|
import { createFateClient } from 'react-fate/client';
|
|
@@ -642,4 +695,37 @@ export function App() {
|
|
|
642
695
|
}
|
|
643
696
|
```
|
|
644
697
|
|
|
698
|
+
```vue [Vue]
|
|
699
|
+
<script setup lang="ts">
|
|
700
|
+
import { httpBatchLink } from '@trpc/client';
|
|
701
|
+
import { computed } from 'vue';
|
|
702
|
+
import { FateClient } from 'vue-fate';
|
|
703
|
+
import { createFateClient } from 'vue-fate/client';
|
|
704
|
+
import AppRoutes from './AppRoutes.vue';
|
|
705
|
+
|
|
706
|
+
const fate = computed(() =>
|
|
707
|
+
createFateClient({
|
|
708
|
+
links: [
|
|
709
|
+
httpBatchLink({
|
|
710
|
+
fetch: (input, init) =>
|
|
711
|
+
fetch(input, {
|
|
712
|
+
...init,
|
|
713
|
+
credentials: 'include',
|
|
714
|
+
}),
|
|
715
|
+
url: `${env('SERVER_URL')}/trpc`,
|
|
716
|
+
}),
|
|
717
|
+
],
|
|
718
|
+
}),
|
|
719
|
+
);
|
|
720
|
+
</script>
|
|
721
|
+
|
|
722
|
+
<template>
|
|
723
|
+
<FateClient :client="fate">
|
|
724
|
+
<AppRoutes />
|
|
725
|
+
</FateClient>
|
|
726
|
+
</template>
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
:::
|
|
730
|
+
|
|
645
731
|
_And you are all set. Happy building!_
|
|
@@ -7,15 +7,25 @@ setup without copying its adapter glue.
|
|
|
7
7
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
::: code-group
|
|
11
|
+
|
|
12
|
+
```sh [React]
|
|
13
|
+
pnpm add @nkzw/fate react-fate void-fate void @void/react
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```sh [Vue]
|
|
17
|
+
pnpm add @nkzw/fate vue-fate void-fate void @void/vue
|
|
12
18
|
```
|
|
13
19
|
|
|
20
|
+
:::
|
|
21
|
+
|
|
14
22
|
## Vite
|
|
15
23
|
|
|
16
|
-
Use the
|
|
24
|
+
Use the framework adapter's Vite plugin with the Void transport:
|
|
17
25
|
|
|
18
|
-
|
|
26
|
+
::: code-group
|
|
27
|
+
|
|
28
|
+
```tsx [React]
|
|
19
29
|
import { voidReact } from '@void/react/plugin';
|
|
20
30
|
import { fate } from 'react-fate/vite';
|
|
21
31
|
import { defineConfig } from 'vite-plus';
|
|
@@ -33,6 +43,26 @@ export default defineConfig({
|
|
|
33
43
|
});
|
|
34
44
|
```
|
|
35
45
|
|
|
46
|
+
```ts [Vue]
|
|
47
|
+
import { voidVue } from '@void/vue/plugin';
|
|
48
|
+
import { fate } from 'vue-fate/vite';
|
|
49
|
+
import { defineConfig } from 'vite-plus';
|
|
50
|
+
import { voidPlugin } from 'void';
|
|
51
|
+
|
|
52
|
+
export default defineConfig({
|
|
53
|
+
plugins: [
|
|
54
|
+
voidPlugin(),
|
|
55
|
+
voidVue(),
|
|
56
|
+
fate({
|
|
57
|
+
module: './src/fate/server.ts',
|
|
58
|
+
transport: 'void',
|
|
59
|
+
}),
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
:::
|
|
65
|
+
|
|
36
66
|
The Void transport uses `/fate` for RPC requests and `/fate-live` for live
|
|
37
67
|
updates by default. In SSR, it calls the exported fate server directly. In the
|
|
38
68
|
browser, it uses fetch and the SSE live endpoint.
|
|
@@ -104,12 +134,14 @@ export const { GET, POST } = defineVoidFateLiveRoute(fateServer, fateLive);
|
|
|
104
134
|
The live route handles `GET /fate-live` SSE connections and `POST /fate-live`
|
|
105
135
|
control messages. `void-fate` does not use WebSockets.
|
|
106
136
|
|
|
107
|
-
##
|
|
137
|
+
## Layout
|
|
108
138
|
|
|
109
|
-
Wrap your app with
|
|
110
|
-
provides the fate client through
|
|
139
|
+
Wrap your app with the Void fate client for your framework. It creates and
|
|
140
|
+
provides the fate client through the matching adapter.
|
|
111
141
|
|
|
112
|
-
|
|
142
|
+
::: code-group
|
|
143
|
+
|
|
144
|
+
```tsx [React]
|
|
113
145
|
import { useShared } from '@void/react';
|
|
114
146
|
import type { ReactNode } from 'react';
|
|
115
147
|
import { VoidFateClient } from 'void-fate/react';
|
|
@@ -128,9 +160,36 @@ export default function Layout({ children }: { children: ReactNode }) {
|
|
|
128
160
|
}
|
|
129
161
|
```
|
|
130
162
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
163
|
+
```vue [Vue]
|
|
164
|
+
<script setup lang="ts">
|
|
165
|
+
import { useShared } from '@void/vue';
|
|
166
|
+
import { computed } from 'vue';
|
|
167
|
+
import { FateClient } from 'vue-fate';
|
|
168
|
+
import { createFateClient } from 'vue-fate/client';
|
|
169
|
+
import type { SharedData } from '../src/lib/shared.ts';
|
|
170
|
+
|
|
171
|
+
const shared = useShared<SharedData>();
|
|
172
|
+
|
|
173
|
+
const fate = computed(() =>
|
|
174
|
+
createFateClient({
|
|
175
|
+
origin: typeof window === 'undefined' ? shared.origin : window.location.origin,
|
|
176
|
+
userId: shared.auth.user?.id,
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
</script>
|
|
180
|
+
|
|
181
|
+
<template>
|
|
182
|
+
<FateClient :client="fate">
|
|
183
|
+
<slot />
|
|
184
|
+
</FateClient>
|
|
185
|
+
</template>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
:::
|
|
189
|
+
|
|
190
|
+
`userId` is optional, but passing it lets the client be recreated when the
|
|
191
|
+
signed-in user changes. Browser requests include credentials when a `userId` is
|
|
192
|
+
present.
|
|
134
193
|
|
|
135
194
|
## Custom Paths
|
|
136
195
|
|
|
@@ -143,12 +202,35 @@ export const fateLive = createVoidFateLive({
|
|
|
143
202
|
});
|
|
144
203
|
```
|
|
145
204
|
|
|
146
|
-
|
|
205
|
+
::: code-group
|
|
206
|
+
|
|
207
|
+
```tsx [React]
|
|
147
208
|
<VoidFateClient livePath="/custom-fate-live" origin={origin} rpcPath="/custom-fate" userId={userId}>
|
|
148
209
|
{children}
|
|
149
210
|
</VoidFateClient>
|
|
150
211
|
```
|
|
151
212
|
|
|
213
|
+
```vue [Vue]
|
|
214
|
+
<script setup lang="ts">
|
|
215
|
+
const fate = computed(() =>
|
|
216
|
+
createFateClient({
|
|
217
|
+
livePath: '/custom-fate-live',
|
|
218
|
+
origin,
|
|
219
|
+
rpcPath: '/custom-fate',
|
|
220
|
+
userId,
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
</script>
|
|
224
|
+
|
|
225
|
+
<template>
|
|
226
|
+
<FateClient :client="fate">
|
|
227
|
+
<slot />
|
|
228
|
+
</FateClient>
|
|
229
|
+
</template>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
:::
|
|
233
|
+
|
|
152
234
|
The route helper does not own the route path. Make sure your Void route filename
|
|
153
235
|
or router configuration matches the paths you pass to the client.
|
|
154
236
|
|
package/lib/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConnectionRef, FateClient as FateClient$1, FateDehydratedState, FateMutations, FateRoots, GraphQLMutationDefinition, GraphQLMutationInput, GraphQLMutationMap, GraphQLMutationOutput, GraphQLTransportOptions, HydrateOptions, HydrationLimits, InferFateAPI, Pagination, Pagination as Pagination$1, Request, RequestOptions, RequestResult, View, ViewData, ViewEntity, ViewEntityName, ViewRef, ViewRef as ViewRef$1, ViewSelection, clientRoot, createClient, createGraphQLTransport, createHTTPTransport, createTRPCTransport, graphqlMutation, mutation, toEntityId, view } from "@nkzw/fate";
|
|
1
|
+
import { ConnectionRef, Deferred, Deferred as Deferred$1, FateClient as FateClient$1, FateDehydratedState, FateMutations, FateRoots, GraphQLMutationDefinition, GraphQLMutationInput, GraphQLMutationMap, GraphQLMutationOutput, GraphQLTransportOptions, HydrateOptions, HydrationLimits, InferFateAPI, Pagination, Pagination as Pagination$1, Request, RequestOptions, RequestResult, View, ViewData, ViewEntity, ViewEntityName, ViewRef, ViewRef as ViewRef$1, ViewSelection, clientRoot, createClient, createGraphQLTransport, createHTTPTransport, createTRPCTransport, defer, graphqlMutation, mutation, toEntityId, view } from "@nkzw/fate";
|
|
2
2
|
import { ReactNode } from "react";
|
|
3
3
|
|
|
4
4
|
//#region src/useRequest.d.ts
|
|
@@ -43,6 +43,7 @@ type ViewEntityWithTypename$1<V extends View<any, any>> = ViewEntity<V> & {
|
|
|
43
43
|
* const post = useLiveView(PostView, postRef);
|
|
44
44
|
*/
|
|
45
45
|
declare function useLiveView<V extends View<any, any>, R extends ViewRef$1<ViewEntityName<V>> | null>(view: V, ref: R): R extends null ? null : ViewData<ViewEntityWithTypename$1<V>, ViewSelection<V>>;
|
|
46
|
+
declare function useLiveView<V extends View<any, any>, R extends Deferred$1<ViewRef$1<ViewEntityName<V>>> | null>(view: V, ref: R): R extends null ? null : ViewData<ViewEntityWithTypename$1<V>, ViewSelection<V>>;
|
|
46
47
|
//#endregion
|
|
47
48
|
//#region src/listView.d.ts
|
|
48
49
|
type ConnectionItems<C> = C extends {
|
|
@@ -56,14 +57,16 @@ type ConnectionSelection = {
|
|
|
56
57
|
};
|
|
57
58
|
//#endregion
|
|
58
59
|
//#region src/useLiveListView.d.ts
|
|
60
|
+
type ConnectionValue$1 = {
|
|
61
|
+
items?: ReadonlyArray<any>;
|
|
62
|
+
pagination?: Pagination$1;
|
|
63
|
+
};
|
|
64
|
+
type ResolvedConnection$1<C> = C extends Deferred$1<infer Value> ? Value : NonNullable<C>;
|
|
59
65
|
/**
|
|
60
66
|
* Subscribes to a connection field, returning live-updating items and pagination
|
|
61
67
|
* helpers to load the next or previous page.
|
|
62
68
|
*/
|
|
63
|
-
declare function useLiveListView<C extends
|
|
64
|
-
items?: ReadonlyArray<any>;
|
|
65
|
-
pagination?: Pagination$1;
|
|
66
|
-
} | null | undefined>(selection: ConnectionSelection, connection: C): [ConnectionItems<NonNullable<C>>, LoadMoreFn | null, LoadMoreFn | null];
|
|
69
|
+
declare function useLiveListView<C extends ConnectionValue$1 | Deferred$1<ConnectionValue$1> | null | undefined>(selection: ConnectionSelection, connection: C): [ConnectionItems<ResolvedConnection$1<C>>, LoadMoreFn | null, LoadMoreFn | null];
|
|
67
70
|
//#endregion
|
|
68
71
|
//#region src/useView.d.ts
|
|
69
72
|
type ViewEntityWithTypename<V extends View<any, any>> = ViewEntity<V> & {
|
|
@@ -76,15 +79,19 @@ type ViewEntityWithTypename<V extends View<any, any>> = ViewEntity<V> & {
|
|
|
76
79
|
* const post = useView(PostView, postRef);
|
|
77
80
|
*/
|
|
78
81
|
declare function useView<V extends View<any, any>, R extends ViewRef$1<ViewEntityName<V>> | null>(view: V, ref: R): R extends null ? null : ViewData<ViewEntityWithTypename<V>, ViewSelection<V>>;
|
|
82
|
+
declare function useView<V extends View<any, any>, R extends Deferred$1<ViewRef$1<ViewEntityName<V>>> | null>(view: V, ref: R): R extends null ? null : ViewData<ViewEntityWithTypename<V>, ViewSelection<V>>;
|
|
83
|
+
declare function useView<V extends View<any, any>>(view: V, ref: Deferred$1<ViewRef$1<ViewEntityName<V>>> | ViewRef$1<ViewEntityName<V>> | null): ViewData<ViewEntityWithTypename<V>, ViewSelection<V>> | null;
|
|
79
84
|
//#endregion
|
|
80
85
|
//#region src/useListView.d.ts
|
|
86
|
+
type ConnectionValue = {
|
|
87
|
+
items?: ReadonlyArray<any>;
|
|
88
|
+
pagination?: Pagination$1;
|
|
89
|
+
};
|
|
90
|
+
type ResolvedConnection<C> = C extends Deferred$1<infer Value> ? Value : NonNullable<C>;
|
|
81
91
|
/**
|
|
82
92
|
* Subscribes to a connection field, returning the current items and pagination
|
|
83
93
|
* helpers to load the next or previous page.
|
|
84
94
|
*/
|
|
85
|
-
declare function useListView<C extends
|
|
86
|
-
items?: ReadonlyArray<any>;
|
|
87
|
-
pagination?: Pagination$1;
|
|
88
|
-
} | null | undefined>(selection: ConnectionSelection, connection: C): [ConnectionItems<NonNullable<C>>, LoadMoreFn | null, LoadMoreFn | null];
|
|
95
|
+
declare function useListView<C extends ConnectionValue | Deferred$1<ConnectionValue> | null | undefined>(selection: ConnectionSelection, connection: C): [ConnectionItems<ResolvedConnection<C>>, LoadMoreFn | null, LoadMoreFn | null];
|
|
89
96
|
//#endregion
|
|
90
|
-
export { type ConnectionRef, FateClient, type FateDehydratedState, type GraphQLMutationDefinition, type GraphQLMutationInput, type GraphQLMutationMap, type GraphQLMutationOutput, type GraphQLTransportOptions, type HydrateOptions, type HydrationLimits, type InferFateAPI, type Pagination, type ViewRef, clientRoot, createClient, createGraphQLTransport, createHTTPTransport, createTRPCTransport, graphqlMutation, mutation, toEntityId, useFateClient, useListView, useLiveListView, useLiveView, useRequest, useView, view };
|
|
97
|
+
export { type ConnectionRef, type Deferred, FateClient, type FateDehydratedState, type GraphQLMutationDefinition, type GraphQLMutationInput, type GraphQLMutationMap, type GraphQLMutationOutput, type GraphQLTransportOptions, type HydrateOptions, type HydrationLimits, type InferFateAPI, type Pagination, type ViewRef, clientRoot, createClient, createGraphQLTransport, createHTTPTransport, createTRPCTransport, defer, graphqlMutation, mutation, toEntityId, useFateClient, useListView, useLiveListView, useLiveView, useRequest, useView, view };
|
package/lib/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConnectionTag, clientRoot, createClient, createGraphQLTransport, createHTTPTransport, createTRPCTransport, graphqlMutation, isViewTag, mutation, toEntityId, view } from "@nkzw/fate";
|
|
1
|
+
import { ConnectionTag, clientRoot, createClient, createGraphQLTransport, createHTTPTransport, createTRPCTransport, defer, graphqlMutation, isDeferred, isViewTag, mutation, toEntityId, view } from "@nkzw/fate";
|
|
2
2
|
import { createContext, use, useCallback, useDeferredValue, useEffect, useEffectEvent, useMemo, useRef, useSyncExternalStore } from "react";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
import { getListEntries } from "@nkzw/fate/list";
|
|
@@ -22,6 +22,16 @@ function useFateClient() {
|
|
|
22
22
|
return context;
|
|
23
23
|
}
|
|
24
24
|
//#endregion
|
|
25
|
+
//#region src/thenable.ts
|
|
26
|
+
const isFulfilledThenable = (value) => "status" in value && value.status === "fulfilled" && "value" in value;
|
|
27
|
+
const fulfilledThenable = (value) => ({
|
|
28
|
+
status: "fulfilled",
|
|
29
|
+
then(onfulfilled, onrejected) {
|
|
30
|
+
return Promise.resolve(value).then(onfulfilled, onrejected);
|
|
31
|
+
},
|
|
32
|
+
value
|
|
33
|
+
});
|
|
34
|
+
//#endregion
|
|
25
35
|
//#region src/useView.tsx
|
|
26
36
|
const nullSnapshot = {
|
|
27
37
|
status: "fulfilled",
|
|
@@ -32,19 +42,99 @@ const nullSnapshot = {
|
|
|
32
42
|
};
|
|
33
43
|
function useView(view, ref) {
|
|
34
44
|
const client = useFateClient();
|
|
45
|
+
const isDeferredRef = isDeferred(ref);
|
|
35
46
|
const snapshotRef = useRef(null);
|
|
47
|
+
const mergedSnapshotRef = useRef(null);
|
|
48
|
+
const pendingRef = useRef(null);
|
|
49
|
+
const readViewSnapshot = useCallback((viewRef, coverage = [], cacheKey) => {
|
|
50
|
+
const snapshot = client.readView(view, viewRef);
|
|
51
|
+
const mergeCoverage = (value) => ({
|
|
52
|
+
...value,
|
|
53
|
+
coverage: coverage.length ? [...coverage, ...value.coverage] : value.coverage
|
|
54
|
+
});
|
|
55
|
+
if (isFulfilledThenable(snapshot)) {
|
|
56
|
+
if (coverage.length) {
|
|
57
|
+
const cached = mergedSnapshotRef.current;
|
|
58
|
+
const resolvedKey = `${viewRef.__typename}:${String(viewRef.id)}`;
|
|
59
|
+
if (cached?.source === snapshot.value && cached.cacheKey === cacheKey && cached.resolvedKey === resolvedKey) {
|
|
60
|
+
snapshotRef.current = cached.thenable.value;
|
|
61
|
+
return cached.thenable;
|
|
62
|
+
}
|
|
63
|
+
const value = mergeCoverage(snapshot.value);
|
|
64
|
+
const thenable = fulfilledThenable(value);
|
|
65
|
+
mergedSnapshotRef.current = {
|
|
66
|
+
cacheKey,
|
|
67
|
+
resolvedKey,
|
|
68
|
+
source: snapshot.value,
|
|
69
|
+
thenable
|
|
70
|
+
};
|
|
71
|
+
snapshotRef.current = value;
|
|
72
|
+
return thenable;
|
|
73
|
+
}
|
|
74
|
+
mergedSnapshotRef.current = null;
|
|
75
|
+
snapshotRef.current = snapshot.value;
|
|
76
|
+
return snapshot;
|
|
77
|
+
}
|
|
78
|
+
mergedSnapshotRef.current = null;
|
|
79
|
+
snapshotRef.current = null;
|
|
80
|
+
return Promise.resolve(snapshot).then((value) => {
|
|
81
|
+
const resolved = mergeCoverage(value);
|
|
82
|
+
snapshotRef.current = resolved;
|
|
83
|
+
return resolved;
|
|
84
|
+
});
|
|
85
|
+
}, [client, view]);
|
|
36
86
|
const getSnapshot = useCallback(() => {
|
|
37
87
|
if (ref === null) {
|
|
38
88
|
snapshotRef.current = null;
|
|
39
89
|
return nullSnapshot;
|
|
40
90
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
91
|
+
if (!isDeferredRef) {
|
|
92
|
+
pendingRef.current = null;
|
|
93
|
+
return readViewSnapshot(ref);
|
|
94
|
+
}
|
|
95
|
+
const deferred = ref;
|
|
96
|
+
const deferredSnapshot = client.readDeferred(deferred);
|
|
97
|
+
if (isFulfilledThenable(deferredSnapshot)) {
|
|
98
|
+
const resolvedRef = deferredSnapshot.value.data;
|
|
99
|
+
pendingRef.current = null;
|
|
100
|
+
if (resolvedRef === null) {
|
|
101
|
+
snapshotRef.current = {
|
|
102
|
+
coverage: deferredSnapshot.value.coverage,
|
|
103
|
+
data: null
|
|
104
|
+
};
|
|
105
|
+
return fulfilledThenable(snapshotRef.current);
|
|
106
|
+
}
|
|
107
|
+
return readViewSnapshot(client.ref(resolvedRef.__typename, resolvedRef.id, view), deferredSnapshot.value.coverage, deferred);
|
|
108
|
+
}
|
|
109
|
+
if (pendingRef.current?.deferred === ref && pendingRef.current.snapshot === deferredSnapshot) return pendingRef.current.viewSnapshot;
|
|
110
|
+
snapshotRef.current = null;
|
|
111
|
+
const viewSnapshot = Promise.resolve(deferredSnapshot).then((deferredValue) => {
|
|
112
|
+
const resolvedRef = deferredValue.data;
|
|
113
|
+
if (resolvedRef === null) {
|
|
114
|
+
const value = {
|
|
115
|
+
coverage: deferredValue.coverage,
|
|
116
|
+
data: null
|
|
117
|
+
};
|
|
118
|
+
snapshotRef.current = value;
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
return Promise.resolve(readViewSnapshot(client.ref(resolvedRef.__typename, resolvedRef.id, view), deferredValue.coverage, deferred)).then((value) => {
|
|
122
|
+
snapshotRef.current = value;
|
|
123
|
+
return value;
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
pendingRef.current = {
|
|
127
|
+
deferred,
|
|
128
|
+
snapshot: deferredSnapshot,
|
|
129
|
+
viewSnapshot
|
|
130
|
+
};
|
|
131
|
+
return viewSnapshot;
|
|
44
132
|
}, [
|
|
45
133
|
client,
|
|
46
134
|
view,
|
|
47
|
-
ref
|
|
135
|
+
ref,
|
|
136
|
+
isDeferredRef,
|
|
137
|
+
readViewSnapshot
|
|
48
138
|
]);
|
|
49
139
|
const snapshot = use(useDeferredValue(useSyncExternalStore(useCallback((onStoreChange) => {
|
|
50
140
|
if (ref === null) {
|
|
@@ -83,12 +173,14 @@ function useView(view, ref) {
|
|
|
83
173
|
//#region src/useLiveView.tsx
|
|
84
174
|
function useLiveView(view, ref) {
|
|
85
175
|
const client = useFateClient();
|
|
86
|
-
const
|
|
87
|
-
const
|
|
176
|
+
const resolvedRef = isDeferred(ref) ? use(client.readDeferred(ref)).data : ref;
|
|
177
|
+
const liveRef = resolvedRef ? client.ref(resolvedRef.__typename, resolvedRef.id, view) : null;
|
|
178
|
+
const liveId = liveRef?.id;
|
|
179
|
+
const liveType = liveRef?.__typename;
|
|
88
180
|
const subscribeLiveView = useEffectEvent(() => {
|
|
89
|
-
if (
|
|
181
|
+
if (liveRef === null) return;
|
|
90
182
|
client.assertLiveViewSupport();
|
|
91
|
-
return client.subscribeLiveView(view,
|
|
183
|
+
return client.subscribeLiveView(view, liveRef);
|
|
92
184
|
});
|
|
93
185
|
useEffect(() => subscribeLiveView(), [
|
|
94
186
|
client,
|
|
@@ -120,11 +212,12 @@ const useListViewInfo = (selection, connection) => ({
|
|
|
120
212
|
*/
|
|
121
213
|
function useListView(selection, connection) {
|
|
122
214
|
const client = useFateClient();
|
|
123
|
-
const
|
|
215
|
+
const resolvedConnection = isDeferred(connection) ? use(client.readDeferred(connection)).data : connection;
|
|
216
|
+
const { metadata, nodeView } = useListViewInfo(selection, resolvedConnection);
|
|
124
217
|
const subscribe = useCallback((onStoreChange) => metadata ? client.store.subscribeList(metadata.key, onStoreChange) : () => {}, [client, metadata]);
|
|
125
218
|
const getSnapshot = useCallback(() => metadata ? client.store.getListState(metadata.key) : void 0, [client, metadata]);
|
|
126
219
|
const listState = useDeferredValue(useSyncExternalStore(subscribe, getSnapshot, getSnapshot));
|
|
127
|
-
const pagination = listState?.pagination ??
|
|
220
|
+
const pagination = listState?.pagination ?? resolvedConnection?.pagination;
|
|
128
221
|
const hasNext = Boolean(pagination?.hasNext);
|
|
129
222
|
const hasPrevious = Boolean(pagination?.hasPrevious);
|
|
130
223
|
const nextCursor = pagination?.nextCursor;
|
|
@@ -135,10 +228,10 @@ function useListView(selection, connection) {
|
|
|
135
228
|
cursor,
|
|
136
229
|
node: client.rootListRef(id, nodeView)
|
|
137
230
|
}));
|
|
138
|
-
return
|
|
231
|
+
return resolvedConnection?.items;
|
|
139
232
|
}, [
|
|
140
233
|
client,
|
|
141
|
-
|
|
234
|
+
resolvedConnection?.items,
|
|
142
235
|
listState,
|
|
143
236
|
metadata,
|
|
144
237
|
nodeView
|
|
@@ -193,7 +286,8 @@ function useListView(selection, connection) {
|
|
|
193
286
|
*/
|
|
194
287
|
function useLiveListView(selection, connection) {
|
|
195
288
|
const client = useFateClient();
|
|
196
|
-
const
|
|
289
|
+
const resolvedConnection = isDeferred(connection) ? use(client.readDeferred(connection)).data : connection;
|
|
290
|
+
const { metadata, nodeView } = useListViewInfo(selection, resolvedConnection);
|
|
197
291
|
const subscribeLiveListView = useEffectEvent(() => {
|
|
198
292
|
if (!metadata) return;
|
|
199
293
|
client.assertLiveConnectionSupport();
|
|
@@ -204,7 +298,7 @@ function useLiveListView(selection, connection) {
|
|
|
204
298
|
metadata?.key,
|
|
205
299
|
nodeView
|
|
206
300
|
]);
|
|
207
|
-
return useListView(selection,
|
|
301
|
+
return useListView(selection, resolvedConnection);
|
|
208
302
|
}
|
|
209
303
|
//#endregion
|
|
210
304
|
//#region src/useRequest.tsx
|
|
@@ -233,4 +327,4 @@ function useRequest(request, options) {
|
|
|
233
327
|
return use(useDeferredValue(promise));
|
|
234
328
|
}
|
|
235
329
|
//#endregion
|
|
236
|
-
export { FateClient, clientRoot, createClient, createGraphQLTransport, createHTTPTransport, createTRPCTransport, graphqlMutation, mutation, toEntityId, useFateClient, useListView, useLiveListView, useLiveView, useRequest, useView, view };
|
|
330
|
+
export { FateClient, clientRoot, createClient, createGraphQLTransport, createHTTPTransport, createTRPCTransport, defer, graphqlMutation, mutation, toEntityId, useFateClient, useListView, useLiveListView, useLiveView, useRequest, useView, view };
|