Skip to content

Commit d2ca86d

Browse files
authored
refactor(store) simplify key value store, add SWR through local storage behavior (#7554)
1 parent 7b9b556 commit d2ca86d

14 files changed

+186
-204
lines changed

packages/sanity/src/core/store/key-value/KeyValueStore.ts

-52
This file was deleted.

packages/sanity/src/core/store/key-value/backends/localStorage.ts

-53
This file was deleted.

packages/sanity/src/core/store/key-value/backends/memory.ts

-26
This file was deleted.

packages/sanity/src/core/store/key-value/backends/types.ts

-15
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export * from './KeyValueStore'
1+
export * from './keyValueStore'
22
export * from './types'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import {type SanityClient} from '@sanity/client'
2+
3+
import {withLocalStorageSWR} from './localStorageSWR'
4+
import {createServerKeyValueStore} from './serverKeyValueStore'
5+
6+
/** @internal */
7+
export function createKeyValueStore(options: {client: SanityClient}) {
8+
return withLocalStorageSWR(createServerKeyValueStore(options))
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {isEqual} from 'lodash'
2+
import {fromEvent, merge, NEVER} from 'rxjs'
3+
import {distinctUntilChanged, filter, map, tap} from 'rxjs/operators'
4+
5+
import {localStoreStorage} from './storage/localStoreStorage'
6+
import {type KeyValueStore, type KeyValueStoreValue} from './types'
7+
8+
// Whether or not to enable instant user sync between tabs
9+
// if set to true, the setting will update instantly across all tabs
10+
const ENABLE_CROSS_TAB_SYNC = false
11+
12+
/**
13+
* Wraps a KeyValueStore and adds Stale-While-Revalidate (SWR) behavior to it
14+
*/
15+
export function withLocalStorageSWR(wrappedStore: KeyValueStore): KeyValueStore {
16+
const storageEvent = ENABLE_CROSS_TAB_SYNC ? fromEvent<StorageEvent>(window, 'storage') : NEVER
17+
18+
function getKey(key: string) {
19+
const lsUpdates = storageEvent.pipe(
20+
filter((event) => event.key === key),
21+
map(() => localStoreStorage.getKey(key)),
22+
)
23+
24+
return merge(lsUpdates, wrappedStore.getKey(key)).pipe(
25+
distinctUntilChanged(isEqual),
26+
tap((value) => {
27+
localStoreStorage.setKey(key, value)
28+
}),
29+
)
30+
}
31+
function setKey(key: string, nextValue: KeyValueStoreValue) {
32+
localStoreStorage.setKey(key, nextValue)
33+
return wrappedStore.setKey(key, nextValue)
34+
}
35+
return {
36+
getKey,
37+
setKey,
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {type SanityClient} from '@sanity/client'
2+
import {isEqual} from 'lodash'
3+
import {concat, type Observable, Subject} from 'rxjs'
4+
import {distinctUntilChanged, filter, map} from 'rxjs/operators'
5+
6+
import {createServerStorage} from './storage/serverStorage'
7+
import {type KeyValueStore, type KeyValueStoreValue} from './types'
8+
9+
export function createServerKeyValueStore({client}: {client: SanityClient}): KeyValueStore {
10+
const serverStorage = createServerStorage({client})
11+
12+
const events$ = new Subject<{
13+
type: 'optimistic' | 'commit'
14+
key: string
15+
value: KeyValueStoreValue
16+
}>()
17+
18+
function getKey(key: string) {
19+
return serverStorage.getKey(key)
20+
}
21+
22+
function setKey(key: string, value: KeyValueStoreValue) {
23+
events$.next({type: 'optimistic', key, value})
24+
25+
/*
26+
* The backend returns the result of the set operation, so we can just pass that along.
27+
* Most utils do not use it (they will take advantage of local state first) but it reflects the
28+
* backend function and could be useful for debugging.
29+
*/
30+
return serverStorage.setKey(key, value).then((storedValue) => {
31+
events$.next({type: 'commit', key, value: storedValue})
32+
return storedValue
33+
})
34+
}
35+
36+
return {
37+
getKey(key: string): Observable<KeyValueStoreValue | null> {
38+
return concat(
39+
getKey(key),
40+
events$.pipe(
41+
filter((event) => event.key === key),
42+
map((event) => event.value),
43+
distinctUntilChanged(isEqual),
44+
),
45+
)
46+
},
47+
setKey,
48+
}
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {supportsLocalStorage} from '../../../util/supportsLocalStorage'
2+
import {type KeyValueStoreValue} from '../types'
3+
import {createMemoryStorage} from './memoryStorage'
4+
5+
function tryParse(val: string) {
6+
try {
7+
return JSON.parse(val)
8+
} catch (err) {
9+
// eslint-disable-next-line no-console
10+
console.warn(`Failed to parse settings: ${err.message}`)
11+
return null
12+
}
13+
}
14+
15+
function createLocalStoreStorage() {
16+
if (!supportsLocalStorage) {
17+
return createMemoryStorage()
18+
}
19+
20+
function getKey(key: string): KeyValueStoreValue | null {
21+
const val = localStorage.getItem(key)
22+
23+
return val === null ? null : tryParse(val)
24+
}
25+
26+
const setKey = function (key: string, nextValue: KeyValueStoreValue) {
27+
// Can't stringify undefined, and nulls are what
28+
// `getItem` returns when key does not exist
29+
if (typeof nextValue === 'undefined' || nextValue === null) {
30+
localStorage.removeItem(key)
31+
} else {
32+
localStorage.setItem(key, JSON.stringify(nextValue))
33+
}
34+
}
35+
return {getKey, setKey}
36+
}
37+
38+
export const localStoreStorage = createLocalStoreStorage()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {type KeyValueStoreValue} from '../types'
2+
3+
export function createMemoryStorage() {
4+
const DB = Object.create(null)
5+
return {
6+
getKey(key: string): KeyValueStoreValue | null {
7+
return DB[key] || null
8+
},
9+
setKey(key: string, value: KeyValueStoreValue) {
10+
DB[key] = value
11+
},
12+
}
13+
}

0 commit comments

Comments
 (0)