Skip to content

Commit

Permalink
Using expiring storage
Browse files Browse the repository at this point in the history
  • Loading branch information
onmax committed Sep 27, 2023
1 parent 7f8a9ec commit 789c688
Show file tree
Hide file tree
Showing 14 changed files with 140 additions and 111 deletions.
2 changes: 1 addition & 1 deletion database/getters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,5 @@ export async function getClusterMaxZoom(dbArgs: DatabaseAuthArgs | DatabaseAnonA
}

export async function getCryptocityPolygon(dbArgs: DatabaseAuthArgs | DatabaseAnonArgs, city: Cryptocity): Promise<FeatureCollection | undefined> {
return await fetchDb<FeatureCollection>(AnonDbFunction.GetCryptocityPolygon, dbArgs, { body: { query: new URLSearchParams({ city }) } })
return await fetchDb<FeatureCollection>(AnonDbFunction.GetCryptocityPolygon, dbArgs, { query: new URLSearchParams({ city }) })
}
10 changes: 4 additions & 6 deletions src/components/DesktopView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import InteractionBar from '@/components/elements/InteractionBar.vue'
import TheMapInstance from '@/components/elements/TheMapInstance.vue'
import IconChevronDown from '@/components/icons/icon-chevron-down.vue'
import { useApp } from '@/stores/app'
import { useCluster } from '@/stores/cluster'
import { useMarkers } from '@/stores/markers'
const { isListShown, firstLocationsLoaded } = storeToRefs(useApp())
const { singlesInView, clustersInView } = storeToRefs(useCluster())
const { isListShown } = storeToRefs(useApp())
const { singlesInView, clustersInView } = storeToRefs(useMarkers())
const openSuggestions = ref(false)
</script>
Expand All @@ -32,9 +32,7 @@ const openSuggestions = ref(false)
<DesktopList :singles="singlesInView" :clusters="clustersInView" :list-is-shown="isListShown" />
</div>
<Button bg-color="white" class="mt-6 shadow" @click="isListShown = !isListShown">
<template v-if="firstLocationsLoaded" #icon>
<IconChevronDown :class="{ 'rotate-180': isListShown }" class="w-2.5 transition-transform delay-500" />
</template>
<template #icon><IconChevronDown :class="{ 'rotate-180': isListShown }" class="w-2.5 transition-transform delay-500" /></template>
<template #label>{{ $t(isListShown ? 'Hide list' : 'Show list') }}</template>
</Button>
</aside>
Expand Down
4 changes: 2 additions & 2 deletions src/components/MobileView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import InteractionBar from '@/components/elements/InteractionBar.vue'
import MobileList from '@/components/elements/MobileList.vue'
import TheMapInstance from '@/components/elements/TheMapInstance.vue'
import { useApp } from '@/stores/app'
import { useCluster } from '@/stores/cluster'
import { useMarkers } from '@/stores/markers'
import { useLocations } from '@/stores/locations'
const { isListShown } = storeToRefs(useApp())
const { singlesInView, clustersInView } = storeToRefs(useCluster())
const { singlesInView, clustersInView } = storeToRefs(useMarkers())
// TODO: Only show list when user searched for something
// watch(firstLocationsLoaded, () => {
Expand Down
4 changes: 2 additions & 2 deletions src/components/elements/FilterModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import CrossIcon from '@/components/icons/icon-cross.vue'
import FilterIcon from '@/components/icons/icon-filter.vue'
import { useFilters } from '@/stores/filters'
import { translateCategory, translateCurrency } from '@/translations'
import { useCluster } from '@/stores/cluster'
import { useMarkers } from '@/stores/markers'
const isOpen = ref(false)
const isMobile = useBreakpoints(screens).smaller('md')
Expand Down Expand Up @@ -42,7 +42,7 @@ const nFilters = computed(() => {
function updateFilters() {
filtersStore.setSelectedCategories(unappliedFiltersCategories.value)
filtersStore.setSelectedCurrencies(unappliedFiltersCurrencies.value)
useCluster().cluster()
useMarkers().cluster()
}
function clearFilters() {
Expand Down
4 changes: 2 additions & 2 deletions src/components/elements/InteractionBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import { useAutocomplete } from '@/composables/useAutocomplete'
import { useMap } from '@/stores/map'
import { useLocations } from '@/stores/locations'
import { useApp } from '@/stores/app'
import { useCluster } from '@/stores/cluster'
import { useMarkers } from '@/stores/markers'
defineEmits({
open: (value: boolean) => value,
})
const { querySearch, status, suggestions } = useAutocomplete()
const { singles } = storeToRefs(useCluster())
const { singles } = storeToRefs(useMarkers())
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
async function onSelect(suggestion?: Suggestion) {
Expand Down
4 changes: 2 additions & 2 deletions src/components/markers/MapMarkers.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import { storeToRefs } from 'pinia'
import ClusterMarkers from './ClusterMarkers.vue'
import SingleMarkers from './SingleMarkers.vue'
import { useCluster } from '@/stores/cluster'
import { useMarkers } from '@/stores/markers'
import { useCryptocity } from '@/stores/cryptocity'
const { clusters, singles } = storeToRefs(useCluster())
const { clusters, singles } = storeToRefs(useMarkers())
const { cryptocities } = storeToRefs(useCryptocity())
// const { zoom } = storeToRefs(useMap())
Expand Down
7 changes: 2 additions & 5 deletions src/composables/useCaptcha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,8 @@ async function _useCaptcha() {
return await grecaptcha.execute(RECAPTCHA_SITE_KEY, { action: 'idle' })
}

async function getCaptchaUuid() {
return await authenticateAnonUser(DATABASE_ARGS, await getCaptchaToken())
}

const { payload: captchaToken } = await useExpiringStorage('captcha_token_uuid', getCaptchaUuid, { expiresIn: CAPTCHA_TOKEN_VALIDITY })
const { payload: captchaToken, init } = useExpiringStorage('captcha_token_uuid', { expiresIn: CAPTCHA_TOKEN_VALIDITY, getAsyncValue: async () => authenticateAnonUser(DATABASE_ARGS, await getCaptchaToken()) })
await init()

// const loadRecaptcha = () => {
// if (loaded)
Expand Down
84 changes: 69 additions & 15 deletions src/composables/useExpiringStorage.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { Serializer } from '@vueuse/core'
import type { Ref } from 'vue'
import { computed, ref, watch } from 'vue'

interface ExpiringValue<T> {
export interface ExpiringValue<T> {
value: T
expires: string
}

interface UseExpiringStorageOptions {
interface UseExpiringStorageBaseOptions<T> {
/**
* The amount of time in ms when the storage expires
*/
Expand All @@ -17,6 +18,40 @@ interface UseExpiringStorageOptions {
* @default true
*/
autoRefresh?: boolean

/**
* The serializer to use
* @default { read: JSON.parse, write: JSON.stringify }
*/
serializer?: Serializer<ExpiringValue<T>>
}

interface UseExpiringStorageSyncOptions<T> extends UseExpiringStorageBaseOptions<T> {
/**
* If provided, it will be used to get the value when the storage is empty and once the storage expires
* @default undefined
*/
getValue?: () => T
}

interface UseExpiringStorageAsyncOptions<T> extends UseExpiringStorageBaseOptions<T> {
/**
* If provided, it will be used to get the value when the storage is empty and once the storage expires.
* If you want to initialize the storage with a value, use init() as follows
* @example
* const { payload, init } = useExpiringStorage('YOUR_KEY', {getAsyncValue: 'your async function'})
* await init() // Initialize the storage with the value if it doesn't exist
*
* @default undefined
*/
getAsyncValue?: () => Promise<T>
}

const storage = globalThis.localStorage
const hasExpired = (expiryDate: string) => new Date(expiryDate).getTime() <= Date.now()
const defaultSerializer: Serializer<ExpiringValue<any>> = {
read: JSON.parse,
write: JSON.stringify,
}

/**
Expand All @@ -27,25 +62,34 @@ interface UseExpiringStorageOptions {
* @param getValue the function to get the value. It will run when the storage is empty and once the storage expires
* @returns
*/
export async function useExpiringStorage<T>(key: string, getValue: () => Promise<T>, options: UseExpiringStorageOptions) {
const { expiresIn, autoRefresh = true } = options
export function useExpiringStorage<T>(key: string, options: UseExpiringStorageSyncOptions<T> | UseExpiringStorageAsyncOptions<T>) {
const { expiresIn, autoRefresh = true, serializer = defaultSerializer as Serializer<ExpiringValue<T>> } = options
if (!(options as UseExpiringStorageSyncOptions<T>).getValue && !(options as UseExpiringStorageAsyncOptions<T>).getAsyncValue)
throw new Error('Either getValue or getAsyncValue must be provided')

const hasExpired = (expiryDate: string) => new Date(expiryDate).getTime() <= Date.now()
const isAsync = 'getAsyncValue' in options
const getValue = isAsync ? (options as UseExpiringStorageAsyncOptions<T>).getAsyncValue! : (options as UseExpiringStorageSyncOptions<T>).getValue!

const storage = globalThis.localStorage

const storedValue = storage.getItem(key) ? JSON.parse(storage.getItem(key)!) as ExpiringValue<T> : undefined
const alreadyExists = storedValue && !hasExpired(storedValue.expires)
const storedValue = storage.getItem(key) ? serializer.read(storage.getItem(key)!) as ExpiringValue<T> : undefined
const alreadyExists = !!storedValue && !hasExpired(storedValue.expires)

// eslint-disable-next-line no-console
console.log(`LocalStorage ${key}: ${alreadyExists ? '♻️ Reusing value' : '🛎️ Creating new one'}`)
console.log(`useExpiringStorage ${key}: ${alreadyExists ? '♻️ Reusing value' : `🛎️ Needs to create a new one. ${'getAsyncValue' in options ? 'Use `await init()` before reading payload.' : ''}`}`)

let initialValue: T | undefined
if (alreadyExists)
initialValue = storedValue!.value
else if (!isAsync)
initialValue = getValue() as T

const stored = ref(alreadyExists ? storedValue.value : await getValue()) as Ref<T>
const stored = ref(initialValue) as Ref<T>

watch(stored, () => {
if (!stored.value)
return
const expires = new Date(Date.now() + expiresIn).toISOString()
storage.setItem(key, JSON.stringify({ value: stored.value, expires }))
}, { immediate: true })
storage.setItem(key, serializer.write({ value: stored.value, expires }))
}, { immediate: true, deep: true })

async function refreshData(expiresIn: number) {
if (autoRefresh) {
Expand All @@ -56,11 +100,21 @@ export async function useExpiringStorage<T>(key: string, getValue: () => Promise
refreshData(expiresIn)
}, expiresIn)
}
};
}

/**
* If the value in the storage has expired or it does not exists, it will be updated with the new value
*/
async function init() {
if (!alreadyExists)
stored.value = await getValue()
}

const remainingTime = alreadyExists && storedValue ? expiresIn - (Date.now() - new Date(storedValue.expires).getTime()) : expiresIn
const remainingTime = alreadyExists && storedValue.value ? new Date(storedValue.expires).getTime() - Date.now() : expiresIn
refreshData(remainingTime)
return {
payload: computed(() => stored.value),
init,
alreadyExists,
}
}
2 changes: 1 addition & 1 deletion src/composables/useInitialMapPosition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ async function selectAndOpenCard(uuid: string) {
if (location) {
// We set the locations in the store, and then we show the card
// https://github.com/nimiq/crypto-map/commit/45b8cdf2e7aabba6039d69043190b8357950736d#r126800610
(await import ('@/stores/cluster')).useCluster().singles = [location]
(await import ('@/stores/markers')).useMarkers().singles = [location]
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
await sleep(500) // We need to wait for the marker to be rendered in the right position to avoid CLS
;(document.querySelector(`[data-trigger-uuid="${uuid}"]`) as HTMLElement)?.click()
Expand Down
16 changes: 5 additions & 11 deletions src/stores/app.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
import { watchOnce } from '@vueuse/core'
import { defineStore, storeToRefs } from 'pinia'
import { ref, watch } from 'vue'
import { useCluster } from './cluster'
import { useMarkers } from '@/stores/markers'

export const useApp = defineStore('app', () => {
// We just track the first load, so we can show a loading indicator
const firstLocationsLoaded = ref(false)
const isListShown = ref(false)
const mapLoaded = ref(false)

const { loaded: markersLoaded } = storeToRefs(useMarkers())

const until = Date.now() + 200 // Show the splash screen at least for 300ms
const showSplashScreen = ref(true)
watch([mapLoaded, firstLocationsLoaded], () => {
if (mapLoaded.value && firstLocationsLoaded.value)
watch([mapLoaded, markersLoaded], () => {
if (mapLoaded.value && markersLoaded.value)
setTimeout(() => showSplashScreen.value = false, Math.max(0, until - Date.now()))
})

const { singles, clusters } = storeToRefs(useCluster())

// The moment any of these two stores are loaded, we set the firstLocationsLoaded to true
watchOnce([singles, clusters], () => firstLocationsLoaded.value = true)

// We track if the user has hidden the search box hint using localStorage
const shouldShowSearchBoxHint = ref(!localStorage.getItem('hideSearchBoxHint'))
document.documentElement.style.setProperty('--search-box-hint', shouldShowSearchBoxHint.value ? '1' : '0')
Expand All @@ -31,7 +26,6 @@ export const useApp = defineStore('app', () => {
}

return {
firstLocationsLoaded,
isListShown,
shouldShowSearchBoxHint,
hideSearchBoxHint,
Expand Down
49 changes: 0 additions & 49 deletions src/stores/captcha.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/stores/cryptocity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import { defineStore, storeToRefs } from 'pinia'
import { addBBoxToArea, bBoxIsWithinArea, bBoxesIntersect, distanceInPx } from 'shared'
import type { BoundingBox, Cryptocity, CryptocityMarker, CryptocityMemoized } from 'types'
import { computed, ref, watch } from 'vue'
import { useCluster } from './cluster'
import { useMarkers } from './markers'
import { useMap } from './map'
import { cryptocitiesData } from '@/assets-dev/cryptocities-assets.ts'
import { getAnonDatabaseArgs } from '@/shared'

export const useCryptocity = defineStore('cryptocities', () => {
const { map, boundingBox, zoom, latInPx, lngInPx } = storeToRefs(useMap())

const { clustersInView } = storeToRefs(useCluster())
const { clustersInView } = storeToRefs(useMarkers())

const cryptocities = useLocalStorage('cryptocities', cryptocitiesData)

Expand Down
6 changes: 3 additions & 3 deletions src/stores/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import type { BoundingBox, EstimatedMapPosition, MapPosition, Point } from 'type
import { computed, ref, shallowRef, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import type { GoogleMap } from 'vue3-google-map'
import { useCluster } from './cluster'
import { useLocations } from './locations'
import { useMarkers } from './markers'

export const useMap = defineStore('map', () => {
const mapInstance = shallowRef<typeof GoogleMap>()
Expand All @@ -30,7 +30,7 @@ export const useMap = defineStore('map', () => {
replace: true,
})
}, 300, { maxWait: 2000 })
const clusterDebouncer = useDebounceFn(() => useCluster().cluster(), 300, { maxWait: 2000 })
const clusterDebouncer = useDebounceFn(() => useMarkers().cluster(), 300, { maxWait: 2000 })

function boundsToBox(bounds: google.maps.LatLngBounds) {
const { lat: swLat, lng: swLng } = bounds.getSouthWest().toJSON()
Expand All @@ -55,7 +55,7 @@ export const useMap = defineStore('map', () => {

// If we don't have the item in the memoized map, we need to update the clusters
// If we have it, getMemoized will update the active value
if (useCluster().needsToUpdate())
if (useMarkers().needsToUpdate())
clusterDebouncer()
}

Expand Down
Loading

0 comments on commit 789c688

Please sign in to comment.