Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Solid fixes (#3850)

authored by

Jovi De Croock and committed by
GitHub
3396ba70 00d66fce

+369 -88
+5
.changeset/late-boats-listen.md
··· 1 + --- 2 + '@urql/solid-start': minor 3 + --- 4 + 5 + Fix SSR runtime failures caused by importing SolidStart's `action` API at module load time by reading `action` from `Provider` context instead.
+5
.changeset/shiny-pets-give.md
··· 1 + --- 2 + '@urql/solid-start': patch 3 + --- 4 + 5 + Fix `createSubscription` to use `@urql/solid-start` context instead of re-exporting the Solid-only implementation from `@urql/solid`.
+35 -27
docs/basics/solid-start.md
··· 61 61 62 62 ### Providing the `Client` 63 63 64 - To make use of the `Client` in SolidStart we will provide it via Solid's Context API using the `Provider` export. The Provider also needs the `query` function from `@solidjs/router`: 64 + To make use of the `Client` in SolidStart we will provide it via Solid's Context API using the `Provider` export. The Provider also needs the `query` and `action` functions from `@solidjs/router`: 65 65 66 66 ```jsx 67 67 // src/root.tsx or src/app.tsx 68 - import { Router, query } from '@solidjs/router'; 68 + import { Router, action, query } from '@solidjs/router'; 69 69 import { FileRoutes } from '@solidjs/start/router'; 70 70 import { Suspense } from 'solid-js'; 71 71 import { createClient, Provider, cacheExchange, fetchExchange } from '@urql/solid-start'; ··· 78 78 export default function App() { 79 79 return ( 80 80 <Router 81 - root={(props) => ( 82 - <Provider value={{ client, query }}> 81 + root={props => ( 82 + <Provider value={{ client, query, action }}> 83 83 <Suspense>{props.children}</Suspense> 84 84 </Provider> 85 85 )} ··· 90 90 } 91 91 ``` 92 92 93 - Now every route and component inside the `Provider` can use GraphQL queries that will be sent to our API. The `query` function is provided in the context so that `createQuery` can access it automatically without manual injection. 93 + Now every route and component inside the `Provider` can use GraphQL queries and mutations that will be sent to our API. The `query` and `action` functions are provided in context so that `createQuery` and `createMutation` can access them automatically. 94 94 95 95 ## Queries 96 96 ··· 135 135 ``` 136 136 137 137 The `createQuery` primitive integrates with SolidStart's data fetching system: 138 + 138 139 1. It wraps SolidStart's `query()` function to execute URQL queries with proper router context 139 140 2. The `query` function is automatically retrieved from the URQL context (no manual injection needed) 140 141 3. The second parameter is a cache key (string) for SolidStart's router ··· 165 166 166 167 export default function TodosPage() { 167 168 const params = useParams(); 168 - 169 + 169 170 const queryTodos = createQuery(TodosListQuery, 'todos-paginated', { 170 171 variables: { 171 172 from: parseInt(params.page) * 10, 172 173 limit: 10, 173 174 }, 174 175 }); 175 - 176 + 176 177 const result = createAsync(() => queryTodos()); 177 178 178 179 return ( ··· 203 204 ``` 204 205 205 206 Available policies: 207 + 206 208 - `cache-first` (default): Prefer cached results, fall back to network 207 209 - `cache-only`: Only use cached results, never send network requests 208 210 - `network-only`: Always send a network request, ignore cache ··· 247 249 const handleRefresh = () => { 248 250 // Invalidate the todos query using keyFor 249 251 const key = keyFor(TodosQuery); 250 - client.reexecuteOperation(client.createRequestOperation('query', { 251 - key, 252 - query: TodosQuery 252 + client.reexecuteOperation(client.createRequestOperation('query', { 253 + key, 254 + query: TodosQuery 253 255 })); 254 256 }; 255 257 ··· 437 439 438 440 const createTodoAction = action(async (formData: FormData) => { 439 441 const title = formData.get('title') as string; 440 - 442 + 441 443 // Perform mutation 442 444 const result = await client.mutation(CreateTodo, { title }).toPromise(); 443 - 445 + 444 446 if (!result.error) { 445 447 // Revalidate multiple routes if needed 446 448 await revalidate(['/todos', '/']); 447 449 } 448 - 450 + 449 451 return result; 450 452 }); 451 453 ``` ··· 453 455 #### Choosing Between Approaches 454 456 455 457 **Use urql's `keyFor` and `reexecuteOperation` when:** 458 + 456 459 - You need to refetch a specific query after a mutation 457 460 - You want fine-grained control over which queries to refresh 458 461 - You're working with multiple queries on the same route and only want to refetch one 459 462 460 463 **Use SolidStart's `revalidate` when:** 464 + 461 465 - You want to refresh all data on a route 462 466 - You're navigating to a different route and want to ensure fresh data 463 467 - You want to leverage SolidStart's routing system for cache management ··· 543 547 ``` 544 548 545 549 The `createMutation` primitive returns a tuple: 550 + 546 551 1. A reactive state object containing `fetching`, `error`, and `data` 547 552 2. An execute function that triggers the mutation 548 553 ··· 576 581 const handleSubmit = async (formData: FormData) => { 577 582 const title = formData.get('title') as string; 578 583 const result = await createTodo({ title }); 579 - 584 + 580 585 if (!result.error) { 581 586 return redirect('/todos'); 582 587 } ··· 623 628 624 629 const handleUpdate = async () => { 625 630 const result = await updateTodo({ id: '1', title: 'Updated' }); 626 - 631 + 627 632 if (result.error) { 628 633 console.error('Oh no!', result.error); 629 634 } else { ··· 641 646 642 647 const handleUpdate = async () => { 643 648 const result = await updateTodo({ id: '1', title: 'Updated' }); 644 - 649 + 645 650 if (result.error) { 646 651 // CombinedError with network or GraphQL errors 647 652 console.error('Mutation failed:', result.error); 648 - 653 + 649 654 // Check for specific error types 650 655 if (result.error.networkError) { 651 656 console.error('Network error:', result.error.networkError); ··· 661 666 662 667 ## Subscriptions 663 668 664 - For GraphQL subscriptions, `@urql/solid-start` re-exports the `createSubscription` primitive from `@urql/solid`, as subscriptions work identically on both client and server: 669 + For GraphQL subscriptions, `@urql/solid-start` provides a `createSubscription` primitive that uses the same SolidStart `Provider` context as `createQuery` and `createMutation`: 665 670 666 671 ```jsx 667 672 import { gql } from '@urql/core'; ··· 696 701 <div> 697 702 <h2>Live Updates</h2> 698 703 <ul> 699 - <For each={todos()}> 700 - {(todo) => <li>{todo.title}</li>} 701 - </For> 704 + <For each={todos()}>{todo => <li>{todo.title}</li>}</For> 702 705 </ul> 703 706 </div> 704 707 ); ··· 739 742 fetchOptions: () => { 740 743 const event = getRequestEvent(); 741 744 const headers: Record<string, string> = {}; 742 - 745 + 743 746 // Forward cookies for authenticated requests 744 747 if (event) { 745 748 const cookie = event.request.headers.get('cookie'); ··· 747 750 headers.cookie = cookie; 748 751 } 749 752 } 750 - 753 + 751 754 return { headers }; 752 755 }, 753 756 }); ··· 757 760 758 761 ### When to Use Each Package 759 762 760 - | Use Case | Package | Why | 761 - |----------|---------|-----| 762 - | Client-side SPA | `@urql/solid` | Optimized for client-only apps, uses SolidJS reactivity patterns | 763 - | SolidStart SSR App | `@urql/solid-start` | Integrates with SolidStart's routing, SSR, and action system | 763 + | Use Case | Package | Why | 764 + | ------------------ | ------------------- | ---------------------------------------------------------------- | 765 + | Client-side SPA | `@urql/solid` | Optimized for client-only apps, uses SolidJS reactivity patterns | 766 + | SolidStart SSR App | `@urql/solid-start` | Integrates with SolidStart's routing, SSR, and action system | 764 767 765 768 ### Key Differences 766 769 767 770 #### Queries 768 771 769 772 **@urql/solid** (Client-side): 773 + 770 774 ```tsx 771 775 import { createQuery } from '@urql/solid'; 772 776 ··· 775 779 ``` 776 780 777 781 **@urql/solid-start** (SSR): 782 + 778 783 ```tsx 779 784 import { createQuery } from '@urql/solid-start'; 780 785 import { createAsync } from '@solidjs/router'; ··· 788 793 #### Mutations 789 794 790 795 **@urql/solid** (Client-side): 796 + 791 797 ```tsx 792 798 import { createMutation } from '@urql/solid'; 793 799 ··· 797 803 ``` 798 804 799 805 **@urql/solid-start** (SSR with Actions): 806 + 800 807 ```tsx 801 808 import { createMutation } from '@urql/solid-start'; 802 809 import { useAction, useSubmission } from '@solidjs/router'; ··· 818 825 ### Migration 819 826 820 827 If you're moving from a SolidJS SPA to SolidStart: 828 + 821 829 1. Change imports from `@urql/solid` to `@urql/solid-start` 822 830 2. Wrap queries with `createAsync()` 823 831 3. Update mutations to use the action pattern with `useAction()` and `useSubmission()`
+2 -2
examples/with-solid-start/src/app.tsx
··· 1 - import { Router, query } from '@solidjs/router'; 1 + import { Router, action, query } from '@solidjs/router'; 2 2 import { FileRoutes } from '@solidjs/start/router'; 3 3 import { Suspense } from 'solid-js'; 4 4 import { ··· 17 17 return ( 18 18 <Router 19 19 root={props => ( 20 - <Provider value={{ client, query }}> 20 + <Provider value={{ client, query, action }}> 21 21 <Suspense>{props.children}</Suspense> 22 22 </Provider> 23 23 )}
+47 -41
packages/solid-start-urql/README.md
··· 29 29 30 30 ### 1. Set up the Provider 31 31 32 - Wrap your app with the `Provider` to make the URQL client and query function available: 32 + Wrap your app with the `Provider` to make the URQL client and router primitives available: 33 33 34 34 ```tsx 35 35 // src/app.tsx 36 - import { Router, query } from '@solidjs/router'; 36 + import { Router, action, query } from '@solidjs/router'; 37 37 import { FileRoutes } from '@solidjs/start/router'; 38 38 import { Provider } from '@urql/solid-start'; 39 39 import { createClient, cacheExchange, fetchExchange } from '@urql/core'; ··· 45 45 46 46 export default function App() { 47 47 return ( 48 - <Router 49 - root={props => ( 50 - <Provider value={{ client, query }}> 51 - {props.children} 52 - </Provider> 53 - )} 54 - > 48 + <Router root={props => <Provider value={{ client, query, action }}>{props.children}</Provider>}> 55 49 <FileRoutes /> 56 50 </Router> 57 51 ); 58 52 } 59 53 ``` 60 54 61 - > **Note:** The Provider now accepts an object with both `client` and `query`. This allows `createQuery` to automatically access the SolidStart query function without manual injection. 55 + > **Note:** The Provider accepts `client`, `query`, and `action` so `createQuery` and `createMutation` can use SolidStart APIs via context. 62 56 63 57 ### 2. Use Queries 64 58 ··· 130 124 const addTodoAction = createMutation(AddTodoMutation, 'add-todo'); 131 125 const addTodo = useAction(addTodoAction); 132 126 const submission = useSubmission(addTodoAction); 133 - 127 + 134 128 let inputRef: HTMLInputElement | undefined; 135 129 136 130 const handleSubmit = async (e: Event) => { ··· 185 179 return ( 186 180 <div> 187 181 <h2>Live Messages</h2> 188 - <For each={messages.data}> 189 - {msg => <div>{msg.text}</div>} 190 - </For> 182 + <For each={messages.data}>{msg => <div>{msg.text}</div>}</For> 191 183 </div> 192 184 ); 193 185 } ··· 200 192 Creates a GraphQL query using SolidStart's `query` and `createAsync` primitives. The `query` function is automatically retrieved from context. 201 193 202 194 **Parameters:** 195 + 203 196 - `queryDocument: DocumentInput` - GraphQL query document 204 197 - `key: string` - Cache key for SolidStart's router 205 198 - `options?: object` - Optional configuration ··· 210 203 **Returns:** A query function that can be used with `createAsync` 211 204 212 205 **Basic Example:** 206 + 213 207 ```tsx 214 208 import { createAsync } from '@solidjs/router'; 215 209 import { createQuery } from '@urql/solid-start'; ··· 217 211 export default function TodosPage() { 218 212 const queryTodos = createQuery(TodosQuery, 'todos-list'); 219 213 const todos = createAsync(() => queryTodos()); 220 - 214 + 221 215 return <div>{/* ... */}</div>; 222 216 } 223 217 ``` 224 218 225 219 **Example with variables:** 220 + 226 221 ```tsx 227 222 import { createAsync } from '@solidjs/router'; 228 223 import { createQuery } from '@urql/solid-start'; ··· 232 227 variables: { id: 1 }, 233 228 }); 234 229 const user = createAsync(() => queryUser()); 235 - 230 + 236 231 return <div>{/* ... */}</div>; 237 232 } 238 233 ``` 239 234 240 235 **Example with custom client:** 236 + 241 237 ```tsx 242 238 import { createAsync } from '@solidjs/router'; 243 239 import { createQuery } from '@urql/solid-start'; ··· 248 244 export default function CustomPage() { 249 245 const queryTodos = createQuery(TodosQuery, 'todos-list'); 250 246 const todos = createAsync(() => queryTodos(customClient)); 251 - 247 + 252 248 return <div>{/* ... */}</div>; 253 249 } 254 250 ``` ··· 260 256 Creates a GraphQL mutation action using SolidStart's `action` primitive. 261 257 262 258 **Args:** 259 + 263 260 - `mutation: DocumentInput` - GraphQL mutation document 264 261 - `key: string` - Cache key for SolidStart's router 265 262 266 263 **Returns:** `Action` - A SolidStart action that can be used with `useAction()` and `useSubmission()` 267 264 268 265 **Example:** 266 + 269 267 ```tsx 270 268 import { createMutation } from '@urql/solid-start'; 271 269 import { useAction, useSubmission } from '@solidjs/router'; ··· 287 285 Creates a GraphQL subscription for real-time updates. 288 286 289 287 **Args:** 288 + 290 289 ```ts 291 290 { 292 291 query: DocumentInput; ··· 297 296 ``` 298 297 299 298 **Handler:** Optional function to accumulate/transform subscription data 299 + 300 300 ```ts 301 - (previousData: Data | undefined, newData: Data) => Data 301 + (previousData: Data | undefined, newData: Data) => Data; 302 302 ``` 303 303 304 304 **Returns:** `[State, ExecuteFunction]` 305 305 306 306 **Example with handler:** 307 + 307 308 ```tsx 308 - const [messages] = createSubscription( 309 - { query: MessagesSubscription }, 310 - (prev = [], data) => [...prev, data.messageAdded] 311 - ); 309 + const [messages] = createSubscription({ query: MessagesSubscription }, (prev = [], data) => [ 310 + ...prev, 311 + data.messageAdded, 312 + ]); 312 313 ``` 313 314 314 315 ### `Provider` 315 316 316 - Context provider for the URQL client and SolidStart query function. 317 + Context provider for the URQL client and SolidStart router primitives. 317 318 318 319 **Props:** 319 - - `value: { client: Client; query: typeof query }` - Object containing the URQL client and query function 320 + 321 + - `value: { client: Client; query: typeof query; action: typeof action }` - Object containing the URQL client and SolidStart router primitives 320 322 321 323 **Example:** 324 + 322 325 ```tsx 323 - import { query } from '@solidjs/router'; 326 + import { action, query } from '@solidjs/router'; 324 327 import { Provider } from '@urql/solid-start'; 325 328 326 - <Provider value={{ client, query }}> 329 + <Provider value={{ client, query, action }}> 327 330 <App /> 328 - </Provider> 331 + </Provider>; 329 332 ``` 330 333 331 334 ### `useClient()` ··· 361 364 362 365 export default function UserPage() { 363 366 const [userId, setUserId] = createSignal(1); 364 - 367 + 365 368 // Create the query function 366 369 const queryUser = createQuery(UserQuery, 'user-details', { 367 370 variables: { id: userId() }, 368 371 }); 369 - 372 + 370 373 // Wrap with createAsync to get reactive data 371 374 const user = createAsync(() => queryUser()); 372 - 375 + 373 376 return ( 374 377 <div> 375 - <button onClick={() => setUserId(userId() + 1)}> 376 - Next User 377 - </button> 378 + <button onClick={() => setUserId(userId() + 1)}>Next User</button> 378 379 <Show when={user()?.data}> 379 380 <h1>{user()!.data.user.name}</h1> 380 381 </Show> ··· 398 399 ``` 399 400 400 401 Choose descriptive cache keys that: 402 + 401 403 - Are unique within your application 402 404 - Describe the data being cached (e.g., 'user-profile', 'todos-list') 403 405 - Make debugging easier by being human-readable ··· 424 426 }); 425 427 }, 426 428 didAuthError(error) { 427 - return error.graphQLErrors.some( 428 - e => e.extensions?.code === 'UNAUTHENTICATED' 429 - ); 429 + return error.graphQLErrors.some(e => e.extensions?.code === 'UNAUTHENTICATED'); 430 430 }, 431 431 async refreshAuth() { 432 432 // Refresh token logic ··· 477 477 478 478 - **`createQuery`** wraps SolidStart's `query()` function to execute URQL queries with automatic SSR and caching. The `query` function is automatically retrieved from the URQL context, eliminating the need for manual injection. 479 479 - **`createMutation`** creates SolidStart `action()` primitives that integrate with `useAction()` and `useSubmission()` for form handling and progressive enhancement 480 - - **`createSubscription`** re-exported from `@urql/solid` (works identically on client/server) 480 + - **`createSubscription`** uses `@urql/solid-start` context so it works with the same Provider as queries and mutations 481 481 482 482 This means you get: 483 + 483 484 - ✅ Automatic server-side rendering 484 485 - ✅ Request deduplication via SolidStart's query caching 485 486 - ✅ Streaming responses ··· 490 491 491 492 ### When to Use Each Package 492 493 493 - | Use Case | Package | Why | 494 - |----------|---------|-----| 495 - | Client-side SPA | `@urql/solid` | Optimized for client-only apps, uses SolidJS reactivity patterns | 496 - | SolidStart SSR App | `@urql/solid-start` | Integrates with SolidStart's routing, SSR, and action system | 494 + | Use Case | Package | Why | 495 + | ------------------ | ------------------- | ---------------------------------------------------------------- | 496 + | Client-side SPA | `@urql/solid` | Optimized for client-only apps, uses SolidJS reactivity patterns | 497 + | SolidStart SSR App | `@urql/solid-start` | Integrates with SolidStart's routing, SSR, and action system | 497 498 498 499 ### Key Differences 499 500 500 501 #### Queries 501 502 502 503 **@urql/solid** (Client-side): 504 + 503 505 ```tsx 504 506 import { createQuery } from '@urql/solid'; 505 507 ··· 508 510 ``` 509 511 510 512 **@urql/solid-start** (SSR): 513 + 511 514 ```tsx 512 515 import { createQuery } from '@urql/solid-start'; 513 516 import { createAsync } from '@solidjs/router'; ··· 521 524 #### Mutations 522 525 523 526 **@urql/solid** (Client-side): 527 + 524 528 ```tsx 525 529 import { createMutation } from '@urql/solid'; 526 530 ··· 530 534 ``` 531 535 532 536 **@urql/solid-start** (SSR with Actions): 537 + 533 538 ```tsx 534 539 import { createMutation } from '@urql/solid-start'; 535 540 import { useAction, useSubmission } from '@solidjs/router'; ··· 551 556 ### Migration 552 557 553 558 If you're moving from a SolidJS SPA to SolidStart: 559 + 554 560 1. Change imports from `@urql/solid` to `@urql/solid-start` 555 561 2. Wrap queries with `createAsync()` 556 562 3. Update mutations to use the action pattern with `useAction()` and `useSubmission()`
+30 -2
packages/solid-start-urql/src/context.test.tsx
··· 2 2 /* @jsxImportSource solid-js */ 3 3 4 4 import { expect, it, describe } from 'vitest'; 5 - import { Provider, useClient } from './context'; 5 + import { Provider, useAction, useClient } from './context'; 6 6 import { renderHook } from '@solidjs/testing-library'; 7 7 import { createClient } from '@urql/core'; 8 8 ··· 16 16 // Mock query function that matches the expected type 17 17 const mockQuery = (fn: any) => fn; 18 18 19 + const mockAction = (fn: any) => fn; 20 + 19 21 const wrapper = (props: { children: any }) => { 20 22 return ( 21 - <Provider value={{ client, query: mockQuery as any }}> 23 + <Provider 24 + value={{ client, query: mockQuery as any, action: mockAction as any }} 25 + > 22 26 {props.children} 23 27 </Provider> 24 28 ); ··· 38 42 }).toThrow(); 39 43 40 44 process.env.NODE_ENV = originalEnv; 45 + }); 46 + 47 + it('should provide action through context', () => { 48 + const client = createClient({ 49 + url: '/graphql', 50 + exchanges: [], 51 + }); 52 + 53 + const mockQuery = (fn: any) => fn; 54 + const mockAction = (fn: any) => fn; 55 + 56 + const wrapper = (props: { children: any }) => { 57 + return ( 58 + <Provider 59 + value={{ client, query: mockQuery as any, action: mockAction as any }} 60 + > 61 + {props.children} 62 + </Provider> 63 + ); 64 + }; 65 + 66 + const { result } = renderHook(() => useAction(), { wrapper }); 67 + 68 + expect(result).toBe(mockAction); 41 69 }); 42 70 });
+13 -2
packages/solid-start-urql/src/context.ts
··· 1 1 import type { Client } from '@urql/core'; 2 2 import { createContext, useContext } from 'solid-js'; 3 - import type { query as defaultQuery } from '@solidjs/router'; 3 + import type { 4 + query as defaultQuery, 5 + action as defaultAction, 6 + } from '@solidjs/router'; 4 7 5 8 export interface UrqlContext { 6 9 client: Client; 7 10 query: typeof defaultQuery; 11 + action: typeof defaultAction; 8 12 } 9 13 10 14 export const Context = createContext<UrqlContext>(); ··· 12 16 13 17 const hasContext = ( 14 18 context: UrqlContext | undefined, 15 - type: 'client' | 'context' 19 + type: 'client' | 'context' | 'action' 16 20 ) => { 17 21 if (process.env.NODE_ENV !== 'production' && context === undefined) { 18 22 const error = `No ${type} has been specified using urql's Provider. Please create a context and add a Provider.`; ··· 35 39 hasContext(context, 'context'); 36 40 return context!.query; 37 41 }; 42 + 43 + export type UseAction = () => typeof defaultAction; 44 + export const useAction: UseAction = () => { 45 + const context = useContext(Context); 46 + hasContext(context, 'action'); 47 + return context!.action; 48 + };
+3 -7
packages/solid-start-urql/src/createMutation.test.ts
··· 12 12 }); 13 13 14 14 vi.mock('./context', () => { 15 - return { 16 - useClient: () => client, 17 - }; 18 - }); 15 + const action = (fn: any, _key?: string) => fn; 19 16 20 - // Mock SolidStart router functions 21 - vi.mock('@solidjs/router', () => { 22 17 return { 23 - action: (fn: any, _key?: string) => fn, 18 + useClient: () => client, 19 + useAction: () => action, 24 20 }; 25 21 }); 26 22
+4 -2
packages/solid-start-urql/src/createMutation.ts
··· 5 5 type OperationResult, 6 6 createRequest, 7 7 } from '@urql/core'; 8 - import { action, type Action } from '@solidjs/router'; 8 + import type { Action } from '@solidjs/router'; 9 9 import { pipe, filter, take, toPromise } from 'wonka'; 10 - import { useClient } from './context'; 10 + import { useAction, useClient } from './context'; 11 11 12 12 export type CreateMutationAction< 13 13 Data = any, ··· 74 74 key: string 75 75 ): CreateMutationAction<Data, Variables> { 76 76 const client = useClient(); 77 + const action = useAction(); 78 + 77 79 return action( 78 80 async (variables: Variables, context?: Partial<OperationContext>) => { 79 81 const request = createRequest(mutation, variables);
+56
packages/solid-start-urql/src/createSubscription.test.ts
··· 1 + // @vitest-environment jsdom 2 + 3 + import { renderHook } from '@solidjs/testing-library'; 4 + import { 5 + OperationResult, 6 + OperationResultSource, 7 + createClient, 8 + gql, 9 + } from '@urql/core'; 10 + import { expect, it, describe, vi } from 'vitest'; 11 + import { makeSubject } from 'wonka'; 12 + import { createSubscription } from './index'; 13 + 14 + const QUERY = gql` 15 + subscription { 16 + value 17 + } 18 + `; 19 + 20 + const client = createClient({ 21 + url: '/graphql', 22 + exchanges: [], 23 + suspense: false, 24 + }); 25 + 26 + vi.mock('./context', () => { 27 + const useClient = () => client; 28 + 29 + return { useClient }; 30 + }); 31 + 32 + describe('createSubscription', () => { 33 + it('should execute against solid-start context client', () => { 34 + const subject = 35 + makeSubject<Pick<OperationResult<{ value: number }, any>, 'data'>>(); 36 + const executeSubscription = vi 37 + .spyOn(client, 'executeSubscription') 38 + .mockImplementation( 39 + () => subject.source as OperationResultSource<OperationResult> 40 + ); 41 + 42 + const { 43 + result: [state], 44 + } = renderHook(() => 45 + createSubscription<{ value: number }, { variable: number }>({ 46 + query: QUERY, 47 + }) 48 + ); 49 + 50 + expect(executeSubscription).toHaveBeenCalledOnce(); 51 + 52 + subject.next({ data: { value: 1 } }); 53 + 54 + expect(state.data).toEqual({ value: 1 }); 55 + }); 56 + });
+159
packages/solid-start-urql/src/createSubscription.ts
··· 1 + import { type MaybeAccessor, asAccessor } from './utils'; 2 + import { 3 + type AnyVariables, 4 + type DocumentInput, 5 + type Operation, 6 + type OperationContext, 7 + type OperationResult, 8 + type CombinedError, 9 + createRequest, 10 + } from '@urql/core'; 11 + import { useClient } from './context'; 12 + import { createStore, produce, reconcile } from 'solid-js/store'; 13 + import { 14 + batch, 15 + createComputed, 16 + createSignal, 17 + onCleanup, 18 + untrack, 19 + } from 'solid-js'; 20 + import { type Source, onEnd, pipe, subscribe } from 'wonka'; 21 + 22 + export type CreateSubscriptionExecute = ( 23 + opts?: Partial<OperationContext> 24 + ) => void; 25 + 26 + export type CreateSubscriptionArgs< 27 + Data, 28 + Variables extends AnyVariables = AnyVariables, 29 + > = { 30 + query: DocumentInput<Data, Variables>; 31 + variables?: MaybeAccessor<Variables>; 32 + context?: MaybeAccessor<Partial<OperationContext>>; 33 + pause?: MaybeAccessor<boolean>; 34 + }; 35 + 36 + export type CreateSubscriptionState< 37 + Data = any, 38 + Variables extends AnyVariables = AnyVariables, 39 + > = { 40 + fetching: boolean; 41 + stale: boolean; 42 + data?: Data; 43 + error?: CombinedError; 44 + extensions?: Record<string, any>; 45 + operation?: Operation<Data, Variables>; 46 + }; 47 + 48 + export type SubscriptionHandler<T, R> = (prev: R | undefined, data: T) => R; 49 + 50 + export type CreateSubscriptionResult< 51 + Data, 52 + Variables extends AnyVariables = AnyVariables, 53 + > = [CreateSubscriptionState<Data, Variables>, CreateSubscriptionExecute]; 54 + 55 + export const createSubscription = < 56 + Data, 57 + Result = Data, 58 + Variables extends AnyVariables = AnyVariables, 59 + >( 60 + args: CreateSubscriptionArgs<Data, Variables>, 61 + handler?: SubscriptionHandler<Data, Result> 62 + ): CreateSubscriptionResult<Result, Variables> => { 63 + const getContext = asAccessor(args.context); 64 + const getPause = asAccessor(args.pause); 65 + const getVariables = asAccessor(args.variables); 66 + 67 + const client = useClient(); 68 + 69 + const request = createRequest(args.query, getVariables() as Variables); 70 + const operation = client.createRequestOperation( 71 + 'subscription', 72 + request, 73 + getContext() 74 + ); 75 + const initialState: CreateSubscriptionState<Result, Variables> = { 76 + operation, 77 + fetching: false, 78 + data: undefined, 79 + error: undefined, 80 + extensions: undefined, 81 + stale: false, 82 + }; 83 + 84 + const [source, setSource] = createSignal< 85 + Source<OperationResult<Data, Variables>> | undefined 86 + >(undefined, { equals: false }); 87 + 88 + const [state, setState] = 89 + createStore<CreateSubscriptionState<Result, Variables>>(initialState); 90 + 91 + createComputed(() => { 92 + if (getPause() === true) { 93 + setSource(undefined); 94 + return; 95 + } 96 + 97 + const context = getContext(); 98 + const request = createRequest(args.query, getVariables() as Variables); 99 + setSource(() => client.executeSubscription(request, context)); 100 + }); 101 + 102 + createComputed(() => { 103 + const s = source(); 104 + if (s === undefined) { 105 + setState('fetching', false); 106 + 107 + return; 108 + } 109 + 110 + setState('fetching', true); 111 + onCleanup( 112 + pipe( 113 + s, 114 + onEnd(() => { 115 + setState( 116 + produce(draft => { 117 + draft.fetching = false; 118 + }) 119 + ); 120 + }), 121 + subscribe(res => { 122 + batch(() => { 123 + if (res.data !== undefined) { 124 + const newData = 125 + typeof handler === 'function' 126 + ? handler( 127 + untrack(() => state.data), 128 + res.data 129 + ) 130 + : (res.data as Result); 131 + setState('data', reconcile(newData)); 132 + } 133 + setState( 134 + produce(draft => { 135 + draft.stale = !!res.stale; 136 + draft.fetching = true; 137 + draft.error = res.error; 138 + draft.operation = res.operation; 139 + draft.extensions = res.extensions; 140 + }) 141 + ); 142 + }); 143 + }) 144 + ).unsubscribe 145 + ); 146 + }); 147 + 148 + const executeSubscription = (opts?: Partial<OperationContext>) => { 149 + const context: Partial<OperationContext> = { 150 + ...getContext(), 151 + ...opts, 152 + }; 153 + const request = createRequest(args.query, getVariables() as Variables); 154 + 155 + setSource(() => client.executeSubscription(request, context)); 156 + }; 157 + 158 + return [state, executeSubscription]; 159 + };
+10 -5
packages/solid-start-urql/src/index.ts
··· 2 2 export * from '@urql/core'; 3 3 4 4 // Context exports 5 - export { type UseClient, type UseQuery, type UrqlContext } from './context'; 6 - export { useClient, useQuery, Provider } from './context'; 5 + export { 6 + type UseAction, 7 + type UseClient, 8 + type UseQuery, 9 + type UrqlContext, 10 + } from './context'; 11 + export { useAction, useClient, useQuery, Provider } from './context'; 7 12 8 13 // Query exports 9 14 export { createQuery } from './createQuery'; ··· 12 17 export { type CreateMutationAction } from './createMutation'; 13 18 export { createMutation } from './createMutation'; 14 19 15 - // Subscription exports - re-exported from @urql/solid (no SolidStart-specific changes needed) 20 + // Subscription exports 16 21 export { 17 22 type CreateSubscriptionArgs, 18 23 type CreateSubscriptionState, 19 24 type CreateSubscriptionExecute, 20 25 type CreateSubscriptionResult, 21 26 type SubscriptionHandler, 22 - createSubscription, 23 - } from '@urql/solid'; 27 + } from './createSubscription'; 28 + export { createSubscription } from './createSubscription'; 24 29 25 30 // Utility exports 26 31 export { type MaybeAccessor } from './utils';