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
@@ -1,6 +1,6 @@
1
1
  # GraphQL Integration
2
2
 
3
- _fate_ can use an existing GraphQL API as its transport. This keeps the React APIs, view composition, normalized cache, masking, requests, list views, live views, and actions the same while replacing the native or tRPC backend with GraphQL operations.
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 React components are allowed to select. `Root` describes the root operations available to `useRequest`. `fateGraphQL.roots` maps those root names to actual GraphQL fields. If the GraphQL field has the same name as the fate root, the `field` entry can be omitted.
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
- ```tsx
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
- ```tsx
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
- Actions use the same `mutation(...)` and `useActionState` APIs described in the [Actions Guide](actions.md).
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 React client.
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-integration.md), which maps fate views and roots to an existing GraphQL schema.
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
- ```tsx
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
- ```tsx
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
- ```tsx
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 React, import the plugin from `@nkzw/fate/vite` and the client APIs from `@nkzw/fate/client`. The plugin wires the same server types for the selected import path.
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 React app with the `FateClient` context provider:
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
- ```tsx
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
- ```sh
11
- pnpm add @nkzw/fate react-fate void-fate void
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 regular `react-fate` Vite plugin with the Void transport:
24
+ Use the framework adapter's Vite plugin with the Void transport:
17
25
 
18
- ```tsx
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
- ## React Layout
137
+ ## Layout
108
138
 
109
- Wrap your app with `VoidFateClient` from `void-fate/react`. It creates and
110
- provides the fate client through `react-fate`:
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
- ```tsx
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
- `userId` is optional, but passing it lets `VoidFateClient` recreate the client
132
- when the signed-in user changes. Browser requests include credentials when a
133
- `userId` is present.
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
- ```tsx
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
- const snapshot = client.readView(view, ref);
42
- snapshotRef.current = snapshot.status === "fulfilled" ? snapshot.value : null;
43
- return snapshot;
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 liveId = ref?.id;
87
- const liveType = ref?.__typename;
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 (ref === null) return;
181
+ if (liveRef === null) return;
90
182
  client.assertLiveViewSupport();
91
- return client.subscribeLiveView(view, ref);
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 { metadata, nodeView } = useListViewInfo(selection, connection);
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 ?? connection?.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 connection?.items;
231
+ return resolvedConnection?.items;
139
232
  }, [
140
233
  client,
141
- connection?.items,
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 { metadata, nodeView } = useListViewInfo(selection, connection);
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, connection);
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 };