From f4516c82d8cc0f2ea097af56e71e3a6df8329765 Mon Sep 17 00:00:00 2001 From: Milan Hauth Date: Tue, 25 Oct 2022 14:45:54 +0200 Subject: [PATCH 01/35] query-core: export type QueryObserverListener --- packages/query-core/src/queryObserver.ts | 5 +---- packages/query-core/src/types.ts | 4 ++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/query-core/src/queryObserver.ts b/packages/query-core/src/queryObserver.ts index 3d1259a64ef..b331b2d2cdf 100644 --- a/packages/query-core/src/queryObserver.ts +++ b/packages/query-core/src/queryObserver.ts @@ -14,6 +14,7 @@ import type { QueryObserverBaseResult, QueryObserverOptions, QueryObserverResult, + QueryObserverListener, QueryOptions, RefetchOptions, } from './types' @@ -23,10 +24,6 @@ import { focusManager } from './focusManager' import { Subscribable } from './subscribable' import { canFetch, isCancelledError } from './retryer' -type QueryObserverListener = ( - result: QueryObserverResult, -) => void - export interface NotifyOptions { cache?: boolean listeners?: boolean diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index 6d3f0ea9ac8..f9f6fa5ee3a 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -455,6 +455,10 @@ export type QueryObserverResult = | QueryObserverLoadingErrorResult | QueryObserverLoadingResult +export type QueryObserverListener = ( + result: QueryObserverResult, +) => void + export interface InfiniteQueryObserverBaseResult< TData = unknown, TError = unknown, From d6b5e3595f3bbd94aea1315f8c11a2d2d51569d3 Mon Sep 17 00:00:00 2001 From: Milan Hauth Date: Tue, 25 Oct 2022 15:04:11 +0200 Subject: [PATCH 02/35] solid-query: add isRestoring --- packages/solid-query/src/createBaseQuery.ts | 34 ++++++++++++++++++--- packages/solid-query/src/index.ts | 5 +++ packages/solid-query/src/isRestoring.ts | 11 +++++++ 3 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 packages/solid-query/src/isRestoring.ts diff --git a/packages/solid-query/src/createBaseQuery.ts b/packages/solid-query/src/createBaseQuery.ts index 209c70532c1..b0e7d481feb 100644 --- a/packages/solid-query/src/createBaseQuery.ts +++ b/packages/solid-query/src/createBaseQuery.ts @@ -1,7 +1,12 @@ import type { QueryObserver } from '@tanstack/query-core' -import type { QueryKey, QueryObserverResult } from '@tanstack/query-core' +import type { + QueryKey, + QueryObserverResult, + QueryObserverListener, +} from '@tanstack/query-core' import type { CreateBaseQueryOptions } from './types' import { useQueryClient } from './QueryClientProvider' +import { useIsRestoring } from './isRestoring' import { onMount, onCleanup, @@ -31,11 +36,15 @@ export function createBaseQuery< Observer: typeof QueryObserver, ): QueryObserverResult { const queryClient = useQueryClient({ context: options.context }) + const isRestoring = useIsRestoring() const emptyData = Symbol('empty') const defaultedOptions = queryClient.defaultQueryOptions(options) - defaultedOptions._optimisticResults = 'optimistic' + defaultedOptions._optimisticResults = isRestoring() + ? 'isRestoring' + : 'optimistic' // Include callbacks in batch renders const observer = new Observer(queryClient, defaultedOptions) + // get the initial result const [state, setState] = createStore>( // @ts-ignore observer.getOptimisticResult(defaultedOptions), @@ -61,7 +70,7 @@ export function createBaseQuery< let taskQueue: Array<() => void> = [] - const unsubscribe = observer.subscribe((result) => { + const handleResult: QueryObserverListener = (result) => { taskQueue.push(() => { batch(() => { const unwrappedResult = { ...unwrap(result) } @@ -85,7 +94,24 @@ export function createBaseQuery< } taskQueue = [] }) - }) + } + + let unsubscribe: () => void = () => undefined + + if (!isRestoring()) { + unsubscribe = observer.subscribe(handleResult) + } else { + createComputed( + on(isRestoring, () => { + if (isRestoring()) { + return + } + // result was restored from cache + handleResult(observer.getCurrentResult()) + unsubscribe = observer.subscribe(handleResult) + }), + ) + } onCleanup(() => unsubscribe()) diff --git a/packages/solid-query/src/index.ts b/packages/solid-query/src/index.ts index 7090337d2ff..608ea056abb 100644 --- a/packages/solid-query/src/index.ts +++ b/packages/solid-query/src/index.ts @@ -17,3 +17,8 @@ export { useIsMutating } from './useIsMutating' export { createMutation } from './createMutation' export { createInfiniteQuery } from './createInfiniteQuery' export { createQueries } from './createQueries' +export { + IsRestoringContext, + IsRestoringProvider, + useIsRestoring, +} from './isRestoring' diff --git a/packages/solid-query/src/isRestoring.ts b/packages/solid-query/src/isRestoring.ts new file mode 100644 index 00000000000..8cb5e7b51a1 --- /dev/null +++ b/packages/solid-query/src/isRestoring.ts @@ -0,0 +1,11 @@ +// based on react-query/src/isRestoring.tsx + +import { createContext, useContext } from 'solid-js' + +const isRestoring = () => false +const IsRestoringContext = createContext(isRestoring) + +const useIsRestoring = () => useContext(IsRestoringContext) +const IsRestoringProvider = IsRestoringContext.Provider + +export { IsRestoringContext, IsRestoringProvider, useIsRestoring } From f5bec64c3c8ed55746ea0d931098b24676f8db54 Mon Sep 17 00:00:00 2001 From: Milan Hauth Date: Tue, 25 Oct 2022 15:04:55 +0200 Subject: [PATCH 03/35] solid-query-persist-client: init at 4.13.0 --- packages/solid-query-persist-client/.eslintrc | 10 + .../solid-query-persist-client/jest-preset.js | 7 + .../solid-query-persist-client/jest.config.ts | 5 + .../solid-query-persist-client/package.json | 52 ++ .../src/PersistQueryClientProvider.tsx | 57 ++ .../PersistQueryClientProvider.test.tsx | 544 ++++++++++++++++++ .../src/__tests__/utils.tsx | 75 +++ .../solid-query-persist-client/src/index.ts | 7 + .../solid-query-persist-client/transform.js | 9 + .../solid-query-persist-client/tsconfig.json | 17 + pnpm-lock.yaml | 11 + 11 files changed, 794 insertions(+) create mode 100644 packages/solid-query-persist-client/.eslintrc create mode 100644 packages/solid-query-persist-client/jest-preset.js create mode 100644 packages/solid-query-persist-client/jest.config.ts create mode 100644 packages/solid-query-persist-client/package.json create mode 100644 packages/solid-query-persist-client/src/PersistQueryClientProvider.tsx create mode 100644 packages/solid-query-persist-client/src/__tests__/PersistQueryClientProvider.test.tsx create mode 100644 packages/solid-query-persist-client/src/__tests__/utils.tsx create mode 100644 packages/solid-query-persist-client/src/index.ts create mode 100644 packages/solid-query-persist-client/transform.js create mode 100644 packages/solid-query-persist-client/tsconfig.json diff --git a/packages/solid-query-persist-client/.eslintrc b/packages/solid-query-persist-client/.eslintrc new file mode 100644 index 00000000000..cefbf99ca1b --- /dev/null +++ b/packages/solid-query-persist-client/.eslintrc @@ -0,0 +1,10 @@ +{ + "parserOptions": { + "project": "./tsconfig.json", + "sourceType": "module" + }, + "rules": { + "react/react-in-jsx-scope": "off", + "react-hooks/rules-of-hooks": "off" + } +} diff --git a/packages/solid-query-persist-client/jest-preset.js b/packages/solid-query-persist-client/jest-preset.js new file mode 100644 index 00000000000..cc0a653b640 --- /dev/null +++ b/packages/solid-query-persist-client/jest-preset.js @@ -0,0 +1,7 @@ +const solidPreset = require('solid-jest/preset/browser/jest-preset') +const tanStackPreset = require('../../jest-preset') + +module.exports = { + ...tanStackPreset, + ...solidPreset, +} diff --git a/packages/solid-query-persist-client/jest.config.ts b/packages/solid-query-persist-client/jest.config.ts new file mode 100644 index 00000000000..46aedae757c --- /dev/null +++ b/packages/solid-query-persist-client/jest.config.ts @@ -0,0 +1,5 @@ +export default { + displayName: 'solid-query', + preset: './jest-preset.js', + transform: { '^.+\\.(ts|tsx)$': './transform.js' }, +} diff --git a/packages/solid-query-persist-client/package.json b/packages/solid-query-persist-client/package.json new file mode 100644 index 00000000000..15c41a4ac55 --- /dev/null +++ b/packages/solid-query-persist-client/package.json @@ -0,0 +1,52 @@ +{ + "name": "@tanstack/solid-query-persist-client", + "version": "4.13.0", + "description": "Solid bindings to work with persisters in TanStack/react-query", + "author": "tannerlinsley", + "license": "MIT", + "repository": "tanstack/query", + "homepage": "https://tanstack.com/query", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "types": "build/lib/index.d.ts", + "main": "build/lib/index.js", + "module": "build/lib/index.esm.js", + "exports": { + ".": { + "types": "./build/lib/index.d.ts", + "solid": "./build/solid/index.js", + "import": "./build/lib/index.mjs", + "browser": "./build/lib/index.mjs", + "require": "./build/lib/index.js", + "node": "./build/lib/index.js", + "default": "./build/lib/index.js" + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "scripts": { + "clean": "rm -rf ./build", + "test:eslint": "../../node_modules/.bin/eslint --ext .ts,.tsx ./src", + "test:jest": "../../node_modules/.bin/jest --config ./jest.config.ts", + "test:dev": "pnpm run test:jest --watch" + }, + "files": [ + "build/lib/*", + "build/umd/*", + "build/solid/*", + "src" + ], + "devDependencies": { + "@tanstack/solid-query": "workspace:*", + "solid-jest": "^0.2.0" + }, + "dependencies": { + "@tanstack/query-persist-client-core": "workspace:*" + }, + "peerDependencies": { + "@tanstack/solid-query": "workspace:*", + "solid-js": "^1.5.7" + } +} diff --git a/packages/solid-query-persist-client/src/PersistQueryClientProvider.tsx b/packages/solid-query-persist-client/src/PersistQueryClientProvider.tsx new file mode 100644 index 00000000000..7043e7b9109 --- /dev/null +++ b/packages/solid-query-persist-client/src/PersistQueryClientProvider.tsx @@ -0,0 +1,57 @@ +// based on react-query-persist-client/src/PersistQueryClientProvider.tsx + +import { createSignal, onMount, onCleanup, mergeProps } from 'solid-js' + +import type { PersistQueryClientOptions } from '@tanstack/query-persist-client-core' +import { persistQueryClient } from '@tanstack/query-persist-client-core' +import type { QueryClientProviderProps } from '@tanstack/solid-query' +import { QueryClientProvider, IsRestoringProvider } from '@tanstack/solid-query' + +export type PersistQueryClientProviderProps = QueryClientProviderProps & { + persistOptions: Omit + onSuccess?: () => void +} + +export const PersistQueryClientProvider = ( + props: PersistQueryClientProviderProps, +) => { + const mergedProps = mergeProps( + { + contextSharing: false, + }, + props, + ) + + const [isRestoring, setIsRestoring] = createSignal(true) + + let isStale = false + + const [unsubscribe, restorePromise] = persistQueryClient({ + ...mergedProps.persistOptions, + queryClient: mergedProps.client, + }) + + restorePromise.then(() => { + if (!isStale) { + mergedProps.onSuccess?.() + setIsRestoring(false) + } + }) + + onMount(() => mergedProps.client.mount()) + + onCleanup(() => { + mergedProps.client.unmount() + + isStale = true + unsubscribe() + }) + + return ( + + + {mergedProps.children} + + + ) +} diff --git a/packages/solid-query-persist-client/src/__tests__/PersistQueryClientProvider.test.tsx b/packages/solid-query-persist-client/src/__tests__/PersistQueryClientProvider.test.tsx new file mode 100644 index 00000000000..786207f5ece --- /dev/null +++ b/packages/solid-query-persist-client/src/__tests__/PersistQueryClientProvider.test.tsx @@ -0,0 +1,544 @@ +// based on react-query-persist-client/src/__tests__/PersistQueryClientProvider.test.tsx + +import { createEffect, createSignal } from 'solid-js' +import { render, screen, waitFor } from 'solid-testing-library' + +import type { + CreateQueryResult, + DefinedCreateQueryResult, +} from '@tanstack/solid-query' +import { + QueryClient, + createQuery, + createQueries, +} from '@tanstack/solid-query' +import type { + PersistedClient, + Persister, +} from '@tanstack/query-persist-client-core' +import type { QueryKey } from '@tanstack/query-core' +import { persistQueryClientSave } from '@tanstack/query-persist-client-core' + +// copy of solid-query/src/__tests__/utils.tsx +import { createQueryClient, mockLogger, queryKey, sleep } from './utils' + +import { PersistQueryClientProvider } from '../PersistQueryClientProvider' + +const createMockPersister = (): Persister => { + let storedState: PersistedClient | undefined + + return { + async persistClient(persistClient: PersistedClient) { + storedState = persistClient + }, + async restoreClient() { + await sleep(10) + return storedState + }, + removeClient() { + storedState = undefined + }, + } +} + +const createMockErrorPersister = ( + removeClient: Persister['removeClient'], +): [Error, Persister] => { + const error = new Error('restore failed') + return [ + error, + { + async persistClient() { + // noop + }, + async restoreClient() { + await sleep(10) + throw error + }, + removeClient, + }, + ] +} + +describe('PersistQueryClientProvider', () => { + test('restores cache from persister', async () => { + const key: () => QueryKey = queryKey() + const states: CreateQueryResult[] = [] + + const queryClient = createQueryClient() + await queryClient.prefetchQuery(key(), () => Promise.resolve('hydrated')) + + const persister = createMockPersister() + + await persistQueryClientSave({ queryClient, persister }) + + queryClient.clear() + + function Page() { + const state = createQuery(key, async () => { + await sleep(10) + return 'fetched' + }) + + states.push(state) + + return ( +
+

{state.data}

+

fetchStatus: {state.fetchStatus}

+
+ ) + } + + render(() => ( + + + + )) + + await waitFor(() => screen.getByText('fetchStatus: idle')) + await waitFor(() => screen.getByText('hydrated')) + await waitFor(() => screen.getByText('fetched')) + + expect(states).toHaveLength(4) + + expect(states[0]).toMatchObject({ + status: 'loading', + fetchStatus: 'idle', + data: undefined, + }) + + expect(states[1]).toMatchObject({ + status: 'success', + fetchStatus: 'fetching', + data: 'hydrated', + }) + + expect(states[2]).toMatchObject({ + status: 'success', + fetchStatus: 'fetching', + data: 'hydrated', + }) + + expect(states[3]).toMatchObject({ + status: 'success', + fetchStatus: 'idle', + data: 'fetched', + }) + }) + + test('should also put createQueries into idle state', async () => { + const key = queryKey() + const states: CreateQueryResult[] = [] + + const queryClient = createQueryClient() + await queryClient.prefetchQuery(key(), () => Promise.resolve('hydrated')) + + const persister = createMockPersister() + + await persistQueryClientSave({ queryClient, persister }) + + queryClient.clear() + + function Page() { + const [state] = createQueries({ + queries: [ + { + queryKey: key, + queryFn: async (): Promise => { + await sleep(10) + return 'fetched' + }, + }, + ], + }) + + states.push(state) + + return ( +
+

{state.data}

+

fetchStatus: {state.fetchStatus}

+
+ ) + } + + render(() => ( + + + + )) + + await waitFor(() => screen.getByText('fetchStatus: idle')) + await waitFor(() => screen.getByText('hydrated')) + await waitFor(() => screen.getByText('fetched')) + + expect(states).toHaveLength(4) + + expect(states[0]).toMatchObject({ + status: 'loading', + fetchStatus: 'idle', + data: undefined, + }) + + expect(states[1]).toMatchObject({ + status: 'success', + fetchStatus: 'fetching', + data: 'hydrated', + }) + + expect(states[2]).toMatchObject({ + status: 'success', + fetchStatus: 'fetching', + data: 'hydrated', + }) + + expect(states[3]).toMatchObject({ + status: 'success', + fetchStatus: 'idle', + data: 'fetched', + }) + }) + + test('should show initialData while restoring', async () => { + const key = queryKey() + const states: DefinedCreateQueryResult[] = [] + + const queryClient = createQueryClient() + await queryClient.prefetchQuery(key(), () => Promise.resolve('hydrated')) + + const persister = createMockPersister() + + await persistQueryClientSave({ queryClient, persister }) + + queryClient.clear() + + function Page() { + const state = createQuery( + key, + async () => { + await sleep(10) + return 'fetched' + }, + { + initialData: 'initial', + // make sure that initial data is older than the hydration data + // otherwise initialData would be newer and takes precedence + initialDataUpdatedAt: 1, + }, + ) + + states.push(state) + + return ( +
+

{state.data}

+

fetchStatus: {state.fetchStatus}

+
+ ) + } + + render(() => ( + + + + )) + + await waitFor(() => screen.getByText('initial')) + await waitFor(() => screen.getByText('hydrated')) + await waitFor(() => screen.getByText('fetched')) + + expect(states).toHaveLength(4) + + expect(states[0]).toMatchObject({ + status: 'success', + fetchStatus: 'idle', + data: 'initial', + }) + + expect(states[1]).toMatchObject({ + status: 'success', + fetchStatus: 'fetching', + data: 'hydrated', + }) + + expect(states[2]).toMatchObject({ + status: 'success', + fetchStatus: 'fetching', + data: 'hydrated', + }) + + expect(states[3]).toMatchObject({ + status: 'success', + fetchStatus: 'idle', + data: 'fetched', + }) + }) + + test('should not refetch after restoring when data is fresh', async () => { + const key = queryKey() + const states: CreateQueryResult[] = [] + + const queryClient = createQueryClient() + await queryClient.prefetchQuery(key(), () => Promise.resolve('hydrated')) + + const persister = createMockPersister() + + await persistQueryClientSave({ queryClient, persister }) + + queryClient.clear() + + function Page() { + const state = createQuery( + key, + async () => { + await sleep(10) + return 'fetched' + }, + { + staleTime: Infinity, + }, + ) + + states.push(state) + + return ( +
+

data: {state.data ?? 'null'}

+

fetchStatus: {state.fetchStatus}

+
+ ) + } + + render(() => ( + + + + )) + + await waitFor(() => screen.getByText('data: null')) + await waitFor(() => screen.getByText('data: hydrated')) + + expect(states).toHaveLength(2) + + expect(states[0]).toMatchObject({ + status: 'loading', + fetchStatus: 'idle', + data: undefined, + }) + + expect(states[1]).toMatchObject({ + status: 'success', + fetchStatus: 'idle', + data: 'hydrated', + }) + }) + + test('should call onSuccess after successful restoring', async () => { + const key = queryKey() + + const queryClient = createQueryClient() + await queryClient.prefetchQuery(key(), () => Promise.resolve('hydrated')) + + const persister = createMockPersister() + + await persistQueryClientSave({ queryClient, persister }) + + queryClient.clear() + + function Page() { + const state = createQuery(key, async () => { + await sleep(10) + return 'fetched' + }) + + return ( +
+

{state.data}

+

fetchStatus: {state.fetchStatus}

+
+ ) + } + + const onSuccess = jest.fn() + + render(() => ( + + + + )) + expect(onSuccess).toHaveBeenCalledTimes(0) + + await waitFor(() => screen.getByText('hydrated')) + expect(onSuccess).toHaveBeenCalledTimes(1) + await waitFor(() => screen.getByText('fetched')) + }) + + test('should remove cache after non-successful restoring', async () => { + const key = queryKey() + jest.spyOn(console, 'warn').mockImplementation(() => undefined) + jest.spyOn(console, 'error').mockImplementation(() => undefined) + + const queryClient = createQueryClient() + const removeClient = jest.fn() + + const [error, persister] = createMockErrorPersister(removeClient) + + function Page() { + const state = createQuery(key, async () => { + await sleep(10) + return 'fetched' + }) + + return ( +
+

{state.data}

+

fetchStatus: {state.fetchStatus}

+
+ ) + } + + render(() => ( + + + + )) + + await waitFor(() => screen.getByText('fetched')) + expect(removeClient).toHaveBeenCalledTimes(1) + expect(mockLogger.error).toHaveBeenCalledTimes(1) + expect(mockLogger.error).toHaveBeenCalledWith(error) + }) + + test('should be able to persist into multiple clients', async () => { + const key = queryKey() + const states: CreateQueryResult[] = [] + + const queryClient = createQueryClient() + await queryClient.prefetchQuery(key(), () => Promise.resolve('hydrated')) + + const persister = createMockPersister() + + await persistQueryClientSave({ queryClient, persister }) + + queryClient.clear() + + const onSuccess = jest.fn() + + const queryFn1 = jest.fn().mockImplementation(async () => { + await sleep(10) + return 'queryFn1' + }) + const queryFn2 = jest.fn().mockImplementation(async () => { + await sleep(10) + return 'queryFn2' + }) + + function App() { + const [client, setClient] = createSignal( + new QueryClient({ + defaultOptions: { + queries: { + queryFn: queryFn1, + }, + }, + }), + ) + + createEffect(() => { + setClient( + new QueryClient({ + defaultOptions: { + queries: { + queryFn: queryFn2, + }, + }, + }), + ) + }, []) + + return ( + + + + ) + } + + function Page() { + const state = createQuery(key) + + states.push(state) + + return ( +
+

{String(state.data)}

+

fetchStatus: {state.fetchStatus}

+
+ ) + } + + render(() => ) + + await waitFor(() => screen.getByText('hydrated')) + await waitFor(() => screen.getByText('queryFn2')) + + expect(queryFn1).toHaveBeenCalledTimes(0) + expect(queryFn2).toHaveBeenCalledTimes(1) + expect(onSuccess).toHaveBeenCalledTimes(1) + + expect(states).toHaveLength(5) + + expect(states[0]).toMatchObject({ + status: 'loading', + fetchStatus: 'idle', + data: undefined, + }) + + expect(states[1]).toMatchObject({ + status: 'loading', + fetchStatus: 'idle', + data: undefined, + }) + + expect(states[2]).toMatchObject({ + status: 'success', + fetchStatus: 'fetching', + data: 'hydrated', + }) + + expect(states[3]).toMatchObject({ + status: 'success', + fetchStatus: 'fetching', + data: 'hydrated', + }) + + expect(states[4]).toMatchObject({ + status: 'success', + fetchStatus: 'idle', + data: 'queryFn2', + }) + }) +}) diff --git a/packages/solid-query-persist-client/src/__tests__/utils.tsx b/packages/solid-query-persist-client/src/__tests__/utils.tsx new file mode 100644 index 00000000000..41c904f127a --- /dev/null +++ b/packages/solid-query-persist-client/src/__tests__/utils.tsx @@ -0,0 +1,75 @@ +import type { QueryClientConfig } from '@tanstack/query-core' +import { QueryClient } from '@tanstack/query-core' +import type { ParentProps } from 'solid-js' +import { createEffect, createSignal, onCleanup, Show } from 'solid-js' + +let queryKeyCount = 0 +export function queryKey(): () => Array { + const localQueryKeyCount = queryKeyCount++ + return () => [`query_${localQueryKeyCount}`] +} + +export const Blink = ( + props: { + duration: number + } & ParentProps, +) => { + const [shouldShow, setShouldShow] = createSignal(true) + + createEffect(() => { + setShouldShow(true) + const timeout = setActTimeout(() => setShouldShow(false), props.duration) + onCleanup(() => clearTimeout(timeout)) + }) + + return ( + off}> + <>{props.children} + + ) +} + +export function createQueryClient(config?: QueryClientConfig): QueryClient { + jest.spyOn(console, 'error').mockImplementation(() => undefined) + return new QueryClient({ logger: mockLogger, ...config }) +} + +export function mockVisibilityState(value: DocumentVisibilityState) { + return jest.spyOn(document, 'visibilityState', 'get').mockReturnValue(value) +} + +export function mockNavigatorOnLine(value: boolean) { + return jest.spyOn(navigator, 'onLine', 'get').mockReturnValue(value) +} + +export const mockLogger = { + log: jest.fn(), + warn: jest.fn(), + error: jest.fn(), +} + +export function sleep(timeout: number): Promise { + return new Promise((resolve, _reject) => { + setTimeout(resolve, timeout) + }) +} + +export function setActTimeout(fn: () => void, ms?: number) { + return setTimeout(() => { + fn() + }, ms) +} + +/** + * Assert the parameter is of a specific type. + */ +export function expectType(_: T): void { + return undefined +} + +/** + * Assert the parameter is not typed as `any` + */ +export function expectTypeNotAny(_: 0 extends 1 & T ? never : T): void { + return undefined +} diff --git a/packages/solid-query-persist-client/src/index.ts b/packages/solid-query-persist-client/src/index.ts new file mode 100644 index 00000000000..15705049b12 --- /dev/null +++ b/packages/solid-query-persist-client/src/index.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ + +// Re-export core +export * from '@tanstack/query-persist-client-core' + +// Solid Query +export * from './PersistQueryClientProvider' diff --git a/packages/solid-query-persist-client/transform.js b/packages/solid-query-persist-client/transform.js new file mode 100644 index 00000000000..d62bbfe50ed --- /dev/null +++ b/packages/solid-query-persist-client/transform.js @@ -0,0 +1,9 @@ +const babelJest = require('babel-jest') + +module.exports = babelJest.default.createTransformer({ + presets: [ + 'babel-preset-solid', + '@babel/preset-env', + '@babel/preset-typescript', + ], +}) diff --git a/packages/solid-query-persist-client/tsconfig.json b/packages/solid-query-persist-client/tsconfig.json new file mode 100644 index 00000000000..c39699e4ca1 --- /dev/null +++ b/packages/solid-query-persist-client/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "rootDir": "./src", + "outDir": "./build/solid", + "declarationDir": "./build/lib", + "tsBuildInfoFile": "./build/.tsbuildinfo", + "jsx": "preserve", + "jsxImportSource": "solid-js", + "emitDeclarationOnly": false + }, + "include": ["src"], + "references": [ + { "path": "../query-core" } + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf06b989e4b..2fce81f8886 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -768,6 +768,17 @@ importers: devDependencies: solid-jest: 0.2.0 + packages/solid-query-persist-client: + specifiers: + '@tanstack/query-persist-client-core': workspace:* + '@tanstack/solid-query': workspace:* + solid-jest: ^0.2.0 + dependencies: + '@tanstack/query-persist-client-core': link:../query-persist-client-core + devDependencies: + '@tanstack/solid-query': link:../solid-query + solid-jest: 0.2.0 + packages/vue-query: specifiers: '@tanstack/match-sorter-utils': ^8.1.1 From b46578773a98d584d583080c50c6518c07508520 Mon Sep 17 00:00:00 2001 From: Milan Hauth Date: Tue, 25 Oct 2022 18:51:02 +0200 Subject: [PATCH 04/35] examples/solid/basic-typescript: move dep to dev-deps --- examples/solid/basic-typescript/package.json | 2 +- pnpm-lock.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/solid/basic-typescript/package.json b/examples/solid/basic-typescript/package.json index 0e265cf44c3..f45f1cf1666 100644 --- a/examples/solid/basic-typescript/package.json +++ b/examples/solid/basic-typescript/package.json @@ -10,10 +10,10 @@ }, "license": "MIT", "dependencies": { - "@tanstack/solid-query": "^4.3.9", "solid-js": "^1.5.1" }, "devDependencies": { + "@tanstack/solid-query": "^4.3.9", "typescript": "^4.8.2", "vite": "^3.0.9", "vite-plugin-solid": "^2.3.9" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2fce81f8886..62838660e4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -599,9 +599,9 @@ importers: vite: ^3.0.9 vite-plugin-solid: ^2.3.9 dependencies: - '@tanstack/solid-query': link:../../../packages/solid-query solid-js: 1.5.4 devDependencies: + '@tanstack/solid-query': link:../../../packages/solid-query typescript: 4.8.3 vite: 3.1.3 vite-plugin-solid: 2.3.9_solid-js@1.5.4+vite@3.1.3 From 7ef366d38653804e24f2a4d01b7c58f793aabdf8 Mon Sep 17 00:00:00 2001 From: Milan Hauth Date: Tue, 25 Oct 2022 19:31:20 +0200 Subject: [PATCH 05/35] examples/solid/offline: init draft --- examples/solid/offline/.eslintrc | 10 + examples/solid/offline/.gitignore | 4 + examples/solid/offline/README.md | 6 + examples/solid/offline/index.html | 16 ++ examples/solid/offline/package.json | 28 ++ examples/solid/offline/src/App.tsx | 260 ++++++++++++++++++ examples/solid/offline/src/api.ts | 82 ++++++ examples/solid/offline/src/assets/favicon.ico | Bin 0 -> 15086 bytes examples/solid/offline/src/index.tsx | 12 + examples/solid/offline/src/movies.ts | 67 +++++ examples/solid/offline/src/persister.ts | 33 +++ examples/solid/offline/tsconfig.json | 15 + examples/solid/offline/vite.config.ts | 15 + pnpm-lock.yaml | 118 ++++---- 14 files changed, 620 insertions(+), 46 deletions(-) create mode 100644 examples/solid/offline/.eslintrc create mode 100644 examples/solid/offline/.gitignore create mode 100644 examples/solid/offline/README.md create mode 100644 examples/solid/offline/index.html create mode 100644 examples/solid/offline/package.json create mode 100644 examples/solid/offline/src/App.tsx create mode 100644 examples/solid/offline/src/api.ts create mode 100644 examples/solid/offline/src/assets/favicon.ico create mode 100644 examples/solid/offline/src/index.tsx create mode 100644 examples/solid/offline/src/movies.ts create mode 100644 examples/solid/offline/src/persister.ts create mode 100644 examples/solid/offline/tsconfig.json create mode 100644 examples/solid/offline/vite.config.ts diff --git a/examples/solid/offline/.eslintrc b/examples/solid/offline/.eslintrc new file mode 100644 index 00000000000..86b22fec59b --- /dev/null +++ b/examples/solid/offline/.eslintrc @@ -0,0 +1,10 @@ +{ + "parserOptions": { + "project": "./tsconfig.json", + "sourceType": "module" + }, + "rules": { + "react/react-in-jsx-scope": "off", + "jsx-a11y/anchor-is-valid": "off" + } +} diff --git a/examples/solid/offline/.gitignore b/examples/solid/offline/.gitignore new file mode 100644 index 00000000000..001e3f924bb --- /dev/null +++ b/examples/solid/offline/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +.yalc +yalc.lock \ No newline at end of file diff --git a/examples/solid/offline/README.md b/examples/solid/offline/README.md new file mode 100644 index 00000000000..310f37f62fd --- /dev/null +++ b/examples/solid/offline/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` +- `npm run start` diff --git a/examples/solid/offline/index.html b/examples/solid/offline/index.html new file mode 100644 index 00000000000..48c59fc1242 --- /dev/null +++ b/examples/solid/offline/index.html @@ -0,0 +1,16 @@ + + + + + + + + Solid App + + + +
+ + + + diff --git a/examples/solid/offline/package.json b/examples/solid/offline/package.json new file mode 100644 index 00000000000..66f6b5c1cce --- /dev/null +++ b/examples/solid/offline/package.json @@ -0,0 +1,28 @@ +{ + "name": "@tanstack/query-example-solid-offline", + "version": "0.0.0", + "description": "", + "scripts": { + "start": "vite", + "dev": "vite", + "build": "vite build", + "serve": "vite preview" + }, + "license": "MIT", + "dependencies": { + "solid-js": "1.5.4" + }, + "devDependencies": { + "@solidjs/router": "^0.5.0", + "@tanstack/solid-query": "workspace:*", + "@tanstack/solid-query-persist-client": "workspace:*", + "@tanstack/query-async-storage-persister": "workspace:*", + "idb-keyval": "^6.2.0", + "ky": "^0.30.0", + "msw": "^0.39.2", + "solid-toast": "^0.3.5", + "typescript": "^4.8.2", + "vite": "^3.0.9", + "vite-plugin-solid": "^2.3.9" + } +} diff --git a/examples/solid/offline/src/App.tsx b/examples/solid/offline/src/App.tsx new file mode 100644 index 00000000000..38b5365c16c --- /dev/null +++ b/examples/solid/offline/src/App.tsx @@ -0,0 +1,260 @@ +/* @refresh reload */ + +import { + createQuery, + QueryClient, + //QueryClientProvider, + MutationCache, + onlineManager, + useIsRestoring, + //useQueryClient, +} from '@tanstack/solid-query' + +import { + PersistQueryClientProvider, +} from '@tanstack/solid-query-persist-client' + +import { createIndexedDBPersister } from './persister' + +import { Component, createSignal, For, Match, Setter, Switch } from 'solid-js' + +// TODO @tanstack/solid-query-devtools +//import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; + +//import toast, { Toaster } from "react-hot-toast"; +import toast, { Toaster } from 'solid-toast'; + +/* +import { + Link, + Outlet, + ReactLocation, + Router, + useMatch, +} from "@tanstack/react-location"; +*/ + +// TODO: A vs Navigate? +// https://github.com/solidjs/solid-router#the-navigate-component +// Solid Router provides a Navigate component that works similarly to A, +// but it will *immediately* navigate to the provided path +// as soon as the component is rendered + +import { Routes, Route, A as Link } from "@solidjs/router"; + +import * as api from "./api"; +import { movieKeys, useMovie } from "./movies"; + +/* TODO? + +import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"; +const persister = createSyncStoragePersister({ + storage: window.localStorage, +}); + +import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister"; +const persister = createAsyncStoragePersister() + +*/ + +const persister = createIndexedDBPersister() + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + cacheTime: 1000 * 60 * 60 * 24, // 24 hours + staleTime: 2000, + retry: 0, + }, + }, + // configure global cache callbacks to show toast notifications + mutationCache: new MutationCache({ + onSuccess: (data: any) => { + toast.success(data.message); + }, + onError: (error: any) => { + toast.error(error.message); + }, + }), +}); + +// we need a default mutation function so that paused mutations can resume after a page reload +queryClient.setMutationDefaults(movieKeys.all(), { + mutationFn: async ({ id, comment }) => { + // to avoid clashes with our optimistic update when an offline mutation continues + await queryClient.cancelQueries(movieKeys.detail(id)); + return api.updateMovie(id, comment); + }, +}); + +export function App() { + return ( + { + // resume mutations after initial restore from localStorage was successful + queryClient.resumePausedMutations().then(() => { + queryClient.invalidateQueries(); + }); + }} + > + + { + // + } + + ); +} + +function Movies() { + const isRestoring = useIsRestoring(); + return ( + <> + + + + queryClient.getQueryData(movieKeys.detail(movieId)) ?? + // do not load if we are offline or hydrating because it returns a promise that is pending until we go online again + // we just let the Detail component handle it + (onlineManager.isOnline() && !isRestoring + ? queryClient.fetchQuery(movieKeys.detail(movieId), () => + api.fetchMovie(movieId) + ) + : undefined)} + /> + This site was made with Solid} + /> + + { + // + } + + + ); +} + +function List() { + const moviesQuery = createQuery( + () => movieKeys.list(), + api.fetchMovies + ); + + if (moviesQuery.isLoading && moviesQuery.isFetching) { + return "Loading..."; + } + + if (moviesQuery.data) { + return ( +
+

Movies

+

+ Try to mock offline behaviour with the button in the devtools. You can + navigate around as long as there is already data in the cache. You'll + get a refetch as soon as you go online again. +

+
    + {moviesQuery.data.movies.map((movie) => ( +
  • + + {movie.title} + +
  • + ))} +
+
+ Updated at: {new Date(moviesQuery.data.ts).toLocaleTimeString()} +
+
{moviesQuery.isFetching && "fetching..."}
+
+ ); + } + + // query will be in 'idle' fetchStatus while restoring from localStorage + return null; +} + +/* TODO +function MovieError() { + const { error } = useMatch(); + + return ( +
+ Back +

Couldn't load movie!

+
{error.message}
+
+ ); +} +*/ + +function Detail(props: any) { + const { comment, setComment, updateMovie, movieQuery } = useMovie(props.movieId); + + if (movieQuery.isLoading && movieQuery.isFetching) { + return "Loading..."; + } + + function submitForm(event: any) { + event.preventDefault(); + + updateMovie.mutate({ + id: props.movieId, + comment, + } as any); + } + + if (movieQuery.data) { + return ( +
+ Back +

Movie: {movieQuery.data.movie.title}

+

+ Try to mock offline behaviour with the button in the devtools, then + update the comment. The optimistic update will succeed, but the actual + mutation will be paused and resumed once you go online again. +

+

+ You can also reload the page, which will make the persisted mutation + resume, as you will be online again when you "come back". +

+

+