-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Subscription mode #1263
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Subscription mode #1263
Changes from 29 commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
cbf8752
update subscribe with middleware
huozhi af89adb
fix lint & test
huozhi 7283270
update typing, fix test and lint
huozhi bfa9056
refactor callback, rename, use refs for callbacks
huozhi 510524a
Delay unmount
huozhi abfa159
Merge branch 'main' into subscribe
huozhi 8ed9d72
Merge branch 'main' into subscribe
huozhi 8f48cce
Merge branch 'main' into subscribe
huozhi 8cfc3f5
no swr destruction and add getters, revert pkg.json
huozhi c023e89
callback -> next
huozhi 2f33952
rename fix types and update test
huozhi 138ede4
change exports to unstable_subscription
huozhi 350f022
Merge branch 'main' into subscribe
huozhi 7ad1935
manage subs with useESE
huozhi beaf868
use serialize
huozhi 0e873eb
merge canary
huozhi 81e2ab1
Merge branch 'main' into subscribe
huozhi 4753152
fix lint
huozhi 401738c
use cache helper to update error
huozhi 6aed4ea
update exports path
huozhi ca2668b
fix script
huozhi c0ab66e
Merge branch 'main' into subscribe
huozhi 8276703
pub check
huozhi c21aa98
add exp jsdoc
huozhi 1c2b974
use cache-scoped storage
shuding 5656fef
use prefixed key; add more tests
shuding 12e3111
fix type
shuding 0c04bfd
remove subscriber ref
shuding 73ffb1c
rename
shuding 7963ae8
fix lint
huozhi 54dfaee
Merge branch 'main' into subscribe
huozhi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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,132 @@ | ||
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>() | ||
]) | ||
} | ||
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)! - 1 | ||
|
||
subscriptions.set(subscriptionKey, count) | ||
|
||
// Dispose if it's the last one. | ||
if (!count) { | ||
disposers.get(subscriptionKey)!() | ||
} | ||
}) | ||
} | ||
|
||
// 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.