Skip to content

Commit ec42fed

Browse files
huozhipromer94shuding
authored
feat: mutate filter (vercel#1989)
* feat: mutate filter * tweak typing * add test * tweak typings * chore: remove useless overload (vercel#1993) * fix typing * chore: change internalMutate type (vercel#1997) * chore: change internalMutate type * chore: useoverload for internal mutate * store keys in global state * add type truthy key * polish truthy key * dont remove key * add clear test case, fix empty key * store key in cache * use values * fix cache test * pass original argument to the filter * fix types * fix types * fix failed tests; use useSyncExternalStore and memorized selector * move function declaration * fix infinite use cases * fix lint error Co-authored-by: Yixuan Xu <[email protected]> Co-authored-by: Shu Ding <[email protected]>
1 parent 518ce25 commit ec42fed

File tree

14 files changed

+522
-210
lines changed

14 files changed

+522
-210
lines changed

_internal/types.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -204,15 +204,13 @@ export type MutatorWrapper<Fn> = Fn extends (
204204
export type Mutator<Data = any> = MutatorWrapper<MutatorFn<Data>>
205205

206206
export interface ScopedMutator<Data = any> {
207-
/** This is used for bound mutator */
208-
(
209-
key: Key,
210-
data?: Data | Promise<Data> | MutatorCallback<Data>,
207+
<T = Data>(
208+
matcher: (key?: Arguments) => boolean,
209+
data?: T | Promise<T> | MutatorCallback<T>,
211210
opts?: boolean | MutatorOptions<Data>
212-
): Promise<Data | undefined>
213-
/** This is used for global mutator */
214-
<T = any>(
215-
key: Key,
211+
): Promise<Array<T | undefined>>
212+
<T = Data>(
213+
key: Arguments,
216214
data?: T | Promise<T> | MutatorCallback<T>,
217215
opts?: boolean | MutatorOptions<Data>
218216
): Promise<T | undefined>
@@ -223,8 +221,6 @@ export type KeyedMutator<Data> = (
223221
opts?: boolean | MutatorOptions<Data>
224222
) => Promise<Data | undefined>
225223

226-
// Public types
227-
228224
export type SWRConfiguration<
229225
Data = any,
230226
Error = any,
@@ -267,6 +263,7 @@ export type RevalidateCallback = <K extends RevalidateEvent>(
267263
) => RevalidateCallbackReturnType[K]
268264

269265
export interface Cache<Data = any> {
266+
keys(): IterableIterator<string>
270267
get(key: Key): State<Data> | undefined
271268
set(key: Key, value: State<Data>): void
272269
delete(key: Key): void

_internal/utils/cache.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const initCache = <Data = any>(
4242
// If there's no global state bound to the provider, create a new one with the
4343
// new mutate function.
4444
const EVENT_REVALIDATORS = {}
45+
4546
const mutate = internalMutate.bind(
4647
UNDEFINED,
4748
provider
@@ -58,16 +59,14 @@ export const initCache = <Data = any>(
5859
subscriptions[key] = subs
5960

6061
subs.push(callback)
61-
return () => {
62-
subs.splice(subs.indexOf(callback), 1)
63-
}
62+
return () => subs.splice(subs.indexOf(callback), 1)
6463
}
6564
const setter = (key: string, value: any, prev: any) => {
6665
provider.set(key, value)
6766
const subs = subscriptions[key]
6867
if (subs) {
6968
for (let i = subs.length; i--; ) {
70-
subs[i](value, prev)
69+
subs[i](prev, value)
7170
}
7271
}
7372
}

_internal/utils/helper.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { SWRGlobalState } from './global-state'
2-
import { Key, Cache, State, GlobalState } from '../types'
2+
import type { Key, Cache, State, GlobalState } from '../types'
33

44
const EMPTY_CACHE = {}
55
export const noop = () => {}
@@ -13,9 +13,12 @@ export const UNDEFINED = (/*#__NOINLINE__*/ noop()) as undefined
1313
export const OBJECT = Object
1414

1515
export const isUndefined = (v: any): v is undefined => v === UNDEFINED
16-
export const isFunction = (v: any): v is Function => typeof v == 'function'
17-
export const isEmptyCache = (v: any): boolean => v === EMPTY_CACHE
18-
export const mergeObjects = (a: any, b: any) => OBJECT.assign({}, a, b)
16+
export const isFunction = <
17+
T extends (...args: any[]) => any = (...args: any[]) => any
18+
>(
19+
v: unknown
20+
): v is T => typeof v == 'function'
21+
export const mergeObjects = (a: any, b?: any) => OBJECT.assign({}, a, b)
1922

2023
const STR_UNDEFINED = 'undefined'
2124

_internal/utils/mutate.ts

Lines changed: 145 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,41 @@ import { SWRGlobalState } from './global-state'
44
import { getTimestamp } from './timestamp'
55
import * as revalidateEvents from '../constants'
66
import {
7-
Key,
87
Cache,
98
MutatorCallback,
109
MutatorOptions,
1110
GlobalState,
12-
State
11+
State,
12+
Arguments,
13+
Key
1314
} from '../types'
1415

15-
export const internalMutate = async <Data>(
16+
type KeyFilter = (key?: Arguments) => boolean
17+
type MutateState<Data> = State<Data, any> & {
18+
// The previously committed data.
19+
_c?: Data
20+
}
21+
22+
export async function internalMutate<Data>(
23+
cache: Cache,
24+
_key: KeyFilter,
25+
_data?: Data | Promise<Data | undefined> | MutatorCallback<Data>,
26+
_opts?: boolean | MutatorOptions<Data>
27+
): Promise<Array<Data | undefined>>
28+
export async function internalMutate<Data>(
29+
cache: Cache,
30+
_key: Arguments,
31+
_data?: Data | Promise<Data | undefined> | MutatorCallback<Data>,
32+
_opts?: boolean | MutatorOptions<Data>
33+
): Promise<Data | undefined>
34+
export async function internalMutate<Data>(
1635
...args: [
17-
Cache,
18-
Key,
19-
undefined | Data | Promise<Data | undefined> | MutatorCallback<Data>,
20-
undefined | boolean | MutatorOptions<Data>
36+
cache: Cache,
37+
_key: KeyFilter | Arguments,
38+
_data?: Data | Promise<Data | undefined> | MutatorCallback<Data>,
39+
_opts?: boolean | MutatorOptions<Data>
2140
]
22-
): Promise<Data | undefined> => {
41+
): Promise<any> {
2342
const [cache, _key, _data, _opts] = args
2443

2544
// When passing as a boolean, it's explicitly used to disable/enable
@@ -35,130 +54,144 @@ export const internalMutate = async <Data>(
3554
const revalidate = options.revalidate !== false
3655
const rollbackOnError = options.rollbackOnError !== false
3756

38-
// Serialize key
39-
const [key] = serialize(_key)
40-
if (!key) return
41-
42-
const [get, set] = createCacheHelper<
43-
Data,
44-
State<Data, any> & {
45-
// The previously committed data.
46-
_c?: Data
47-
}
48-
>(cache, key)
49-
const [EVENT_REVALIDATORS, MUTATION, FETCH] = SWRGlobalState.get(
50-
cache
51-
) as GlobalState
52-
53-
const revalidators = EVENT_REVALIDATORS[key]
54-
const startRevalidate = () => {
55-
if (revalidate) {
56-
// Invalidate the key by deleting the concurrent request markers so new
57-
// requests will not be deduped.
58-
delete FETCH[key]
59-
if (revalidators && revalidators[0]) {
60-
return revalidators[0](revalidateEvents.MUTATE_EVENT).then(
61-
() => get().data
62-
)
57+
// If the second argument is a key filter, return the mutation results for all
58+
// filtered keys.
59+
if (isFunction(_key)) {
60+
const keyFilter = _key
61+
const matchedKeys: Key[] = []
62+
for (const key of cache.keys()) {
63+
if (
64+
// Skip the speical useSWRInfinite keys.
65+
!key.startsWith('$inf$') &&
66+
keyFilter((cache.get(key) as { _k: Arguments })._k)
67+
) {
68+
matchedKeys.push(key)
6369
}
6470
}
65-
return get().data
71+
return Promise.all(matchedKeys.map(mutateByKey))
6672
}
6773

68-
// If there is no new data provided, revalidate the key with current state.
69-
if (args.length < 3) {
70-
// Revalidate and broadcast state.
71-
return startRevalidate()
72-
}
74+
return mutateByKey(_key)
75+
76+
async function mutateByKey(_k: Key): Promise<Data | undefined> {
77+
// Serialize key
78+
const [key] = serialize(_k)
79+
if (!key) return
80+
const [get, set] = createCacheHelper<Data, MutateState<Data>>(cache, key)
81+
const [EVENT_REVALIDATORS, MUTATION, FETCH] = SWRGlobalState.get(
82+
cache
83+
) as GlobalState
84+
85+
const revalidators = EVENT_REVALIDATORS[key]
86+
const startRevalidate = () => {
87+
if (revalidate) {
88+
// Invalidate the key by deleting the concurrent request markers so new
89+
// requests will not be deduped.
90+
delete FETCH[key]
91+
if (revalidators && revalidators[0]) {
92+
return revalidators[0](revalidateEvents.MUTATE_EVENT).then(
93+
() => get().data
94+
)
95+
}
96+
}
97+
return get().data
98+
}
7399

74-
let data: any = _data
75-
let error: unknown
100+
// If there is no new data provided, revalidate the key with current state.
101+
if (args.length < 3) {
102+
// Revalidate and broadcast state.
103+
return startRevalidate()
104+
}
76105

77-
// Update global timestamps.
78-
const beforeMutationTs = getTimestamp()
79-
MUTATION[key] = [beforeMutationTs, 0]
106+
let data: any = _data
107+
let error: unknown
80108

81-
const hasOptimisticData = !isUndefined(optimisticData)
82-
const state = get()
109+
// Update global timestamps.
110+
const beforeMutationTs = getTimestamp()
111+
MUTATION[key] = [beforeMutationTs, 0]
83112

84-
// `displayedData` is the current value on screen. It could be the optimistic value
85-
// that is going to be overridden by a `committedData`, or get reverted back.
86-
// `committedData` is the validated value that comes from a fetch or mutation.
87-
const displayedData = state.data
88-
const committedData = isUndefined(state._c) ? displayedData : state._c
113+
const hasOptimisticData = !isUndefined(optimisticData)
114+
const state = get()
89115

90-
// Do optimistic data update.
91-
if (hasOptimisticData) {
92-
optimisticData = isFunction(optimisticData)
93-
? optimisticData(committedData)
94-
: optimisticData
116+
// `displayedData` is the current value on screen. It could be the optimistic value
117+
// that is going to be overridden by a `committedData`, or get reverted back.
118+
// `committedData` is the validated value that comes from a fetch or mutation.
119+
const displayedData = state.data
120+
const committedData = isUndefined(state._c) ? displayedData : state._c
95121

96-
// When we set optimistic data, backup the current committedData data in `_c`.
97-
set({ data: optimisticData, _c: committedData })
98-
}
122+
// Do optimistic data update.
123+
if (hasOptimisticData) {
124+
optimisticData = isFunction(optimisticData)
125+
? optimisticData(committedData)
126+
: optimisticData
99127

100-
if (isFunction(data)) {
101-
// `data` is a function, call it passing current cache value.
102-
try {
103-
data = (data as MutatorCallback<Data>)(committedData)
104-
} catch (err) {
105-
// If it throws an error synchronously, we shouldn't update the cache.
106-
error = err
128+
// When we set optimistic data, backup the current committedData data in `_c`.
129+
set({ data: optimisticData, _c: committedData })
107130
}
108-
}
109131

110-
// `data` is a promise/thenable, resolve the final data first.
111-
if (data && isFunction((data as Promise<Data>).then)) {
112-
// This means that the mutation is async, we need to check timestamps to
113-
// avoid race conditions.
114-
data = await (data as Promise<Data>).catch(err => {
115-
error = err
116-
})
117-
118-
// Check if other mutations have occurred since we've started this mutation.
119-
// If there's a race we don't update cache or broadcast the change,
120-
// just return the data.
121-
if (beforeMutationTs !== MUTATION[key][0]) {
122-
if (error) throw error
123-
return data
124-
} else if (error && hasOptimisticData && rollbackOnError) {
125-
// Rollback. Always populate the cache in this case but without
126-
// transforming the data.
127-
populateCache = true
128-
data = committedData
129-
130-
// Reset data to be the latest committed data, and clear the `_c` value.
131-
set({ data, _c: UNDEFINED })
132+
if (isFunction(data)) {
133+
// `data` is a function, call it passing current cache value.
134+
try {
135+
data = (data as MutatorCallback<Data>)(committedData)
136+
} catch (err) {
137+
// If it throws an error synchronously, we shouldn't update the cache.
138+
error = err
139+
}
132140
}
133-
}
134141

135-
// If we should write back the cache after request.
136-
if (populateCache) {
137-
if (!error) {
138-
// Transform the result into data.
139-
if (isFunction(populateCache)) {
140-
data = populateCache(data, committedData)
142+
// `data` is a promise/thenable, resolve the final data first.
143+
if (data && isFunction((data as Promise<Data>).then)) {
144+
// This means that the mutation is async, we need to check timestamps to
145+
// avoid race conditions.
146+
data = await (data as Promise<Data>).catch(err => {
147+
error = err
148+
})
149+
150+
// Check if other mutations have occurred since we've started this mutation.
151+
// If there's a race we don't update cache or broadcast the change,
152+
// just return the data.
153+
if (beforeMutationTs !== MUTATION[key][0]) {
154+
if (error) throw error
155+
return data
156+
} else if (error && hasOptimisticData && rollbackOnError) {
157+
// Rollback. Always populate the cache in this case but without
158+
// transforming the data.
159+
populateCache = true
160+
data = committedData
161+
162+
// Reset data to be the latest committed data, and clear the `_c` value.
163+
set({ data, _c: UNDEFINED })
141164
}
142-
143-
// Only update cached data if there's no error. Data can be `undefined` here.
144-
set({ data, _c: UNDEFINED })
145165
}
146166

147-
// Always update error and original data here.
148-
set({ error })
149-
}
167+
// If we should write back the cache after request.
168+
if (populateCache) {
169+
if (!error) {
170+
// Transform the result into data.
171+
if (isFunction(populateCache)) {
172+
data = populateCache(data, committedData)
173+
}
150174

151-
// Reset the timestamp to mark the mutation has ended.
152-
MUTATION[key][1] = getTimestamp()
175+
// Only update cached data if there's no error. Data can be `undefined` here.
176+
set({ data, _c: UNDEFINED })
177+
}
153178

154-
// Update existing SWR Hooks' internal states:
155-
const res = await startRevalidate()
179+
// Always update error and original data here.
180+
set({ error })
181+
}
182+
183+
// Reset the timestamp to mark the mutation has ended.
184+
MUTATION[key][1] = getTimestamp()
156185

157-
// The mutation and revalidation are ended, we can clear it since the data is
158-
// not an optimistic value anymore.
159-
set({ _c: UNDEFINED })
186+
// Update existing SWR Hooks' internal states:
187+
const res = await startRevalidate()
160188

161-
// Throw error or return data
162-
if (error) throw error
163-
return populateCache ? res : data
189+
// The mutation and revalidation are ended, we can clear it since the data is
190+
// not an optimistic value anymore.
191+
set({ _c: UNDEFINED })
192+
193+
// Throw error or return data
194+
if (error) throw error
195+
return populateCache ? res : data
196+
}
164197
}

0 commit comments

Comments
 (0)