-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* update subscribe with middleware * fix lint & test * update typing, fix test and lint * refactor callback, rename, use refs for callbacks * Delay unmount * no swr destruction and add getters, revert pkg.json * callback -> next * rename fix types and update test * change exports to unstable_subscription * manage subs with useESE * use serialize * fix lint * use cache helper to update error * update exports path * fix script * pub check * add exp jsdoc * use cache-scoped storage * use prefixed key; add more tests * fix type * remove subscriber ref * rename * fix lint --------- Co-authored-by: Shu Ding <g@shud.in>
- Loading branch information
Showing
8 changed files
with
316 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import type { Key, SWRHook, Middleware, SWRConfiguration, SWRConfig } from 'swr' | ||
|
||
import useSWR from 'swr' | ||
import { | ||
withMiddleware, | ||
serialize, | ||
useIsomorphicLayoutEffect, | ||
createCacheHelper | ||
} from 'swr/_internal' | ||
|
||
export type SWRSubscription<Data = any, Error = any> = ( | ||
key: Key, | ||
{ next }: { next: (err?: Error | null, data?: Data) => void } | ||
) => () => void | ||
|
||
export type SWRSubscriptionResponse<Data = any, Error = any> = { | ||
data?: Data | ||
error?: Error | ||
} | ||
|
||
export type SWRSubscriptionHook<Data = any, Error = any> = ( | ||
key: Key, | ||
subscribe: SWRSubscription<Data, Error>, | ||
config?: SWRConfiguration | ||
) => SWRSubscriptionResponse<Data, Error> | ||
|
||
// [subscription count, disposer] | ||
type SubscriptionStates = [Map<string, number>, Map<string, () => void>] | ||
const subscriptionStorage = new WeakMap<object, SubscriptionStates>() | ||
|
||
const SUBSCRIPTION_PREFIX = '$sub$' | ||
|
||
export const subscription = (<Data, Error>(useSWRNext: SWRHook) => | ||
( | ||
_key: Key, | ||
subscribe: SWRSubscription<Data, Error>, | ||
config: SWRConfiguration & typeof SWRConfig.defaultValue | ||
): SWRSubscriptionResponse<Data, Error> => { | ||
const [key] = serialize(_key) | ||
|
||
// Prefix the key to avoid conflicts with other SWR resources. | ||
const subscriptionKey = key ? SUBSCRIPTION_PREFIX + key : undefined | ||
const swr = useSWRNext(subscriptionKey, null, config) | ||
|
||
const { cache } = config | ||
|
||
// Ensure that the subscription state is scoped by the cache boundary, so | ||
// you can have multiple SWR zones with subscriptions having the same key. | ||
if (!subscriptionStorage.has(cache)) { | ||
subscriptionStorage.set(cache, [ | ||
new Map<string, number>(), | ||
new Map<string, () => void>() | ||
]) | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
const [subscriptions, disposers] = subscriptionStorage.get(cache)! | ||
|
||
useIsomorphicLayoutEffect(() => { | ||
if (!subscriptionKey) return | ||
|
||
const [, set] = createCacheHelper<Data>(cache, subscriptionKey) | ||
const refCount = subscriptions.get(subscriptionKey) || 0 | ||
|
||
const next = (error?: Error | null, data?: Data) => { | ||
if (error !== null && typeof error !== 'undefined') { | ||
set({ error }) | ||
} else { | ||
swr.mutate(data, false) | ||
} | ||
} | ||
|
||
// Increment the ref count. | ||
subscriptions.set(subscriptionKey, refCount + 1) | ||
|
||
if (!refCount) { | ||
const dispose = subscribe(key, { next }) | ||
if (typeof dispose !== 'function') { | ||
throw new Error( | ||
'The `subscribe` function must return a function to unsubscribe.' | ||
) | ||
} | ||
disposers.set(subscriptionKey, dispose) | ||
} | ||
|
||
return () => { | ||
// Prevent frequent unsubscribe caused by unmount | ||
setTimeout(() => { | ||
// TODO: Throw error during development if count is undefined. | ||
const count = subscriptions.get(subscriptionKey) | ||
if (count == null) return | ||
|
||
subscriptions.set(subscriptionKey, count - 1) | ||
|
||
// Dispose if it's the last one. | ||
if (count === 1) { | ||
const dispose = disposers.get(subscriptionKey) | ||
dispose?.() | ||
} | ||
}) | ||
} | ||
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [subscriptionKey]) | ||
|
||
return { | ||
get data() { | ||
return swr.data | ||
}, | ||
get error() { | ||
return swr.error | ||
} | ||
} | ||
}) as unknown as Middleware | ||
|
||
/** | ||
* A hook to subscribe a SWR resource to an external data source for continuous updates. | ||
* @experimental This API is experimental and might change in the future. | ||
* @example | ||
* ```jsx | ||
* import useSWRSubscription from 'swr/subscription' | ||
* | ||
* const { data, error } = useSWRSubscription(key, (key, { next }) => { | ||
* const unsubscribe = dataSource.subscribe(key, (err, data) => { | ||
* next(err, data) | ||
* }) | ||
* return unsubscribe | ||
* }) | ||
* ``` | ||
*/ | ||
const useSWRSubscription = withMiddleware( | ||
useSWR, | ||
subscription | ||
) as SWRSubscriptionHook | ||
|
||
export default useSWRSubscription |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"main": "./dist/index.js", | ||
"module": "./dist/index.esm.js", | ||
"types": "./dist/index.d.ts", | ||
"exports": "./dist/index.mjs", | ||
"private": true, | ||
"scripts": { | ||
"watch": "bunchee index.ts -w", | ||
"build": "bunchee index.ts", | ||
"types:check": "tsc --noEmit", | ||
"clean": "rimraf dist" | ||
}, | ||
"peerDependencies": { | ||
"swr": "*", | ||
"react": "*" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"extends": "../tsconfig.json", | ||
"compilerOptions": { | ||
"outDir": "./dist", | ||
"rootDir": "..", | ||
}, | ||
"include": [".", "../src"], | ||
"exclude": ["./dist"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
import React from 'react' | ||
import { act, screen } from '@testing-library/react' | ||
import { sleep, renderWithConfig, createKey } from './utils' | ||
|
||
import useSWRSubscription from 'swr/subscription' | ||
import useSWR from 'swr' | ||
|
||
describe('useSWRSubscription', () => { | ||
it('should update the state', async () => { | ||
const swrKey = createKey() | ||
|
||
let intervalId | ||
let res = 0 | ||
function subscribe(key, { next }) { | ||
intervalId = setInterval(() => { | ||
if (res === 3) { | ||
const err = new Error(key + 'error') | ||
next(err) | ||
} else { | ||
next(undefined, key + res) | ||
} | ||
res++ | ||
}, 100) | ||
|
||
return () => {} | ||
} | ||
|
||
function Page() { | ||
const { data, error } = useSWRSubscription(swrKey, subscribe, { | ||
fallbackData: 'fallback' | ||
}) | ||
return <div>{error ? error.message : data}</div> | ||
} | ||
|
||
renderWithConfig(<Page />) | ||
await act(() => sleep(10)) | ||
screen.getByText(`fallback`) | ||
await act(() => sleep(100)) | ||
screen.getByText(`${swrKey}0`) | ||
await act(() => sleep(100)) | ||
screen.getByText(`${swrKey}1`) | ||
await act(() => sleep(100)) | ||
screen.getByText(`${swrKey}2`) | ||
await act(() => sleep(100)) | ||
screen.getByText(`${swrKey}error`) | ||
clearInterval(intervalId) | ||
await sleep(100) | ||
screen.getByText(`${swrKey}error`) | ||
}) | ||
|
||
it('should deduplicate subscriptions', async () => { | ||
const swrKey = createKey() | ||
|
||
let subscriptionCount = 0 | ||
|
||
function subscribe(key, { next }) { | ||
++subscriptionCount | ||
let res = 0 | ||
const intervalId = setInterval(() => { | ||
if (res === 3) { | ||
const err = new Error(key + 'error') | ||
next(err) | ||
} else { | ||
next(undefined, key + res) | ||
} | ||
res++ | ||
}, 100) | ||
|
||
return () => { | ||
clearInterval(intervalId) | ||
} | ||
} | ||
|
||
function Page() { | ||
const { data, error } = useSWRSubscription(swrKey, subscribe, { | ||
fallbackData: 'fallback' | ||
}) | ||
useSWRSubscription(swrKey, subscribe) | ||
useSWRSubscription(swrKey, subscribe) | ||
|
||
return <div>{error ? error.message : data}</div> | ||
} | ||
|
||
renderWithConfig(<Page />) | ||
await act(() => sleep(10)) | ||
screen.getByText(`fallback`) | ||
await act(() => sleep(100)) | ||
screen.getByText(`${swrKey}0`) | ||
await act(() => sleep(100)) | ||
screen.getByText(`${swrKey}1`) | ||
await act(() => sleep(100)) | ||
screen.getByText(`${swrKey}2`) | ||
|
||
expect(subscriptionCount).toBe(1) | ||
}) | ||
|
||
it('should not conflict with useSWR state', async () => { | ||
const swrKey = createKey() | ||
|
||
function subscribe(key, { next }) { | ||
let res = 0 | ||
const intervalId = setInterval(() => { | ||
if (res === 3) { | ||
const err = new Error(key + 'error') | ||
next(err) | ||
} else { | ||
next(undefined, key + res) | ||
} | ||
res++ | ||
}, 100) | ||
|
||
return () => { | ||
clearInterval(intervalId) | ||
} | ||
} | ||
|
||
function Page() { | ||
const { data, error } = useSWRSubscription(swrKey, subscribe, { | ||
fallbackData: 'fallback' | ||
}) | ||
const { data: swrData } = useSWR(swrKey, () => 'swr') | ||
return ( | ||
<div> | ||
{swrData}:{error ? error.message : data} | ||
</div> | ||
) | ||
} | ||
|
||
renderWithConfig(<Page />) | ||
await act(() => sleep(10)) | ||
screen.getByText(`swr:fallback`) | ||
await act(() => sleep(100)) | ||
screen.getByText(`swr:${swrKey}0`) | ||
await act(() => sleep(100)) | ||
screen.getByText(`swr:${swrKey}1`) | ||
await act(() => sleep(100)) | ||
screen.getByText(`swr:${swrKey}2`) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters