init
This commit is contained in:
52
src/index.tsx
Normal file
52
src/index.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Hono } from 'hono'
|
||||
import { renderer } from './renderer'
|
||||
import { createApp } from './main';
|
||||
import { renderToWebStream } from 'vue/server-renderer';
|
||||
import { streamText } from 'hono/streaming';
|
||||
import { renderSSRHead } from '@unhead/vue/server';
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
// app.use(renderer)
|
||||
|
||||
// app.get('/', (c) => {
|
||||
// return c.text('Hello World!')
|
||||
// // return c.render(<h1>Hello!</h1>)
|
||||
// })
|
||||
app.get("*", async (c) => {
|
||||
const url = new URL(c.req.url);
|
||||
const { app, router, head } = createApp();
|
||||
router.push(url.pathname);
|
||||
await router.isReady();
|
||||
return streamText(c, async (stream) => {
|
||||
c.header("Content-Type", "text/html; charset=utf-8");
|
||||
c.header("Content-Encoding", "Identity");
|
||||
const ctx = {};
|
||||
const appStream = renderToWebStream(app, ctx);
|
||||
await stream.write("<!DOCTYPE html><html lang='en'><head>");
|
||||
await stream.write("<base href='" + url.origin + "'/>");
|
||||
await renderSSRHead(head).then((headString) => stream.write(headString.headTags.replace(/\n/g, "")));
|
||||
await stream.write(`<link
|
||||
href="https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"rel="stylesheet"></link>`);
|
||||
await stream.write("</head><body class='font-sans bg-[#f9fafd] text-gray-800 antialiased flex flex-col'>");
|
||||
await stream.pipe(appStream);
|
||||
let json = htmlEscape(JSON.stringify(JSON.stringify(ctx)));
|
||||
await stream.write(`<script>window.__SSR_STATE__ = JSON.parse(${json});</script>`);
|
||||
await stream.write("</body></html>");
|
||||
});
|
||||
// return c.body(renderToWebStream(app, {}));
|
||||
})
|
||||
const ESCAPE_LOOKUP: { [match: string]: string } = {
|
||||
"&": "\\u0026",
|
||||
">": "\\u003e",
|
||||
"<": "\\u003c",
|
||||
"\u2028": "\\u2028",
|
||||
"\u2029": "\\u2029",
|
||||
};
|
||||
|
||||
const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
|
||||
|
||||
function htmlEscape(str: string): string {
|
||||
return str.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]);
|
||||
}
|
||||
export default app
|
||||
36
src/lib/hoc/withErrorBoundary.tsx
Normal file
36
src/lib/hoc/withErrorBoundary.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { defineComponent, onErrorCaptured, ref } from "vue";
|
||||
|
||||
export function withErrorBoundary(WrappedComponent: any) {
|
||||
return defineComponent({
|
||||
name: `WithErrorBoundary(${WrappedComponent.name})`,
|
||||
render() {
|
||||
if (this.hasError) {
|
||||
return (
|
||||
<div class="flex items-center justify-center min-h-screen-sm">
|
||||
<div class="p-8 space-y-lg max-w-lg w-full">
|
||||
<p>
|
||||
<b>500. </b>
|
||||
<ins class="text-gray-500 decoration-none">Đã xảy ra lỗi.</ins>
|
||||
</p>
|
||||
<div class="font-thin">
|
||||
<p>Máy chủ đang gặp sự cố tạm thời và không thể xử lý yêu cầu của bạn. Vui lòng <a class="underline text-primary" href="/">thử lại</a> sau vài phút.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <WrappedComponent {...this.$props} v-slots={this.$slots} />;
|
||||
},
|
||||
setup() {
|
||||
const hasError = ref(false);
|
||||
const error = ref<Error | null>(null);
|
||||
onErrorCaptured((err) => {
|
||||
hasError.value = true;
|
||||
error.value = err as Error;
|
||||
console.error("Error captured in withErrorBoundary:", err);
|
||||
return false; // Prevent further propagation
|
||||
});
|
||||
return { hasError, error, reset: () => { hasError.value = false; error.value = null; } };
|
||||
},
|
||||
});
|
||||
}
|
||||
77
src/lib/swr/cache/adapters/localStorage.ts
vendored
Normal file
77
src/lib/swr/cache/adapters/localStorage.ts
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
import SWRVCache, { type ICacheItem } from '..'
|
||||
import type { IKey } from '../../types'
|
||||
|
||||
/**
|
||||
* LocalStorage cache adapter for swrv data cache.
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
|
||||
*/
|
||||
export default class LocalStorageCache extends SWRVCache<any> {
|
||||
private STORAGE_KEY
|
||||
|
||||
constructor (key = 'swrv', ttl = 0) {
|
||||
super(ttl)
|
||||
this.STORAGE_KEY = key
|
||||
}
|
||||
|
||||
private encode (storage: any) { return JSON.stringify(storage) }
|
||||
private decode (storage: any) { return JSON.parse(storage) }
|
||||
|
||||
get (k: IKey): ICacheItem<IKey> {
|
||||
const item = localStorage.getItem(this.STORAGE_KEY)
|
||||
if (item) {
|
||||
const _key = this.serializeKey(k)
|
||||
const itemParsed: ICacheItem<any> = JSON.parse(item)[_key]
|
||||
|
||||
if (itemParsed?.expiresAt === null) {
|
||||
itemParsed.expiresAt = Infinity // localStorage sets Infinity to 'null'
|
||||
}
|
||||
|
||||
return itemParsed
|
||||
}
|
||||
|
||||
return undefined as any
|
||||
}
|
||||
|
||||
set (k: string, v: any, ttl: number) {
|
||||
let payload = {}
|
||||
const _key = this.serializeKey(k)
|
||||
const timeToLive = ttl || this.ttl
|
||||
const storage = localStorage.getItem(this.STORAGE_KEY)
|
||||
const now = Date.now()
|
||||
const item = {
|
||||
data: v,
|
||||
createdAt: now,
|
||||
expiresAt: timeToLive ? now + timeToLive : Infinity
|
||||
}
|
||||
|
||||
if (storage) {
|
||||
payload = this.decode(storage)
|
||||
(payload as any)[_key] = item
|
||||
} else {
|
||||
payload = { [_key]: item }
|
||||
}
|
||||
|
||||
this.dispatchExpire(timeToLive, item, _key)
|
||||
localStorage.setItem(this.STORAGE_KEY, this.encode(payload))
|
||||
}
|
||||
|
||||
dispatchExpire (ttl: number, item: any, serializedKey: string) {
|
||||
ttl && setTimeout(() => {
|
||||
const current = Date.now()
|
||||
const hasExpired = current >= item.expiresAt
|
||||
if (hasExpired) this.delete(serializedKey)
|
||||
}, ttl)
|
||||
}
|
||||
|
||||
delete (serializedKey: string) {
|
||||
const storage = localStorage.getItem(this.STORAGE_KEY)
|
||||
let payload = {} as Record<string, any>
|
||||
|
||||
if (storage) {
|
||||
payload = this.decode(storage)
|
||||
delete payload[serializedKey]
|
||||
}
|
||||
|
||||
localStorage.setItem(this.STORAGE_KEY, this.encode(payload))
|
||||
}
|
||||
}
|
||||
72
src/lib/swr/cache/index.ts
vendored
Normal file
72
src/lib/swr/cache/index.ts
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { IKey } from '../types'
|
||||
import hash from '../lib/hash'
|
||||
export interface ICacheItem<Data> {
|
||||
data: Data,
|
||||
createdAt: number,
|
||||
expiresAt: number
|
||||
}
|
||||
|
||||
function serializeKeyDefault (key: IKey): string {
|
||||
if (typeof key === 'function') {
|
||||
try {
|
||||
key = key()
|
||||
} catch (err) {
|
||||
// dependencies not ready
|
||||
key = ''
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(key)) {
|
||||
key = hash(key)
|
||||
} else {
|
||||
// convert null to ''
|
||||
key = String(key || '')
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
export default class SWRVCache<CacheData> {
|
||||
protected ttl: number
|
||||
private items?: Map<string, ICacheItem<CacheData>>
|
||||
|
||||
constructor (ttl = 0) {
|
||||
this.items = new Map()
|
||||
this.ttl = ttl
|
||||
}
|
||||
|
||||
serializeKey (key: IKey): string {
|
||||
return serializeKeyDefault(key)
|
||||
}
|
||||
|
||||
get (k: string): ICacheItem<CacheData> {
|
||||
const _key = this.serializeKey(k)
|
||||
return this.items!.get(_key)!
|
||||
}
|
||||
|
||||
set (k: string, v: any, ttl: number) {
|
||||
const _key = this.serializeKey(k)
|
||||
const timeToLive = ttl || this.ttl
|
||||
const now = Date.now()
|
||||
const item = {
|
||||
data: v,
|
||||
createdAt: now,
|
||||
expiresAt: timeToLive ? now + timeToLive : Infinity
|
||||
}
|
||||
|
||||
this.dispatchExpire(timeToLive, item, _key)
|
||||
this.items!.set(_key, item)
|
||||
}
|
||||
|
||||
dispatchExpire (ttl: number, item: any, serializedKey: string) {
|
||||
ttl && setTimeout(() => {
|
||||
const current = Date.now()
|
||||
const hasExpired = current >= item.expiresAt
|
||||
if (hasExpired) this.delete(serializedKey)
|
||||
}, ttl)
|
||||
}
|
||||
|
||||
delete (serializedKey: string) {
|
||||
this.items!.delete(serializedKey)
|
||||
}
|
||||
}
|
||||
8
src/lib/swr/index.ts
Normal file
8
src/lib/swr/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import SWRVCache from './cache'
|
||||
import useSWRV, { mutate } from './use-swrv'
|
||||
|
||||
export {
|
||||
type IConfig
|
||||
} from './types'
|
||||
export { mutate, SWRVCache }
|
||||
export default useSWRV
|
||||
44
src/lib/swr/lib/hash.ts
Normal file
44
src/lib/swr/lib/hash.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// From https://github.com/vercel/swr/blob/master/src/libs/hash.ts
|
||||
// use WeakMap to store the object->key mapping
|
||||
// so the objects can be garbage collected.
|
||||
// WeakMap uses a hashtable under the hood, so the lookup
|
||||
// complexity is almost O(1).
|
||||
const table = new WeakMap()
|
||||
|
||||
// counter of the key
|
||||
let counter = 0
|
||||
|
||||
// hashes an array of objects and returns a string
|
||||
export default function hash (args: any[]): string {
|
||||
if (!args.length) return ''
|
||||
let key = 'arg'
|
||||
for (let i = 0; i < args.length; ++i) {
|
||||
let _hash
|
||||
if (
|
||||
args[i] === null ||
|
||||
(typeof args[i] !== 'object' && typeof args[i] !== 'function')
|
||||
) {
|
||||
// need to consider the case that args[i] is a string:
|
||||
// args[i] _hash
|
||||
// "undefined" -> '"undefined"'
|
||||
// undefined -> 'undefined'
|
||||
// 123 -> '123'
|
||||
// null -> 'null'
|
||||
// "null" -> '"null"'
|
||||
if (typeof args[i] === 'string') {
|
||||
_hash = '"' + args[i] + '"'
|
||||
} else {
|
||||
_hash = String(args[i])
|
||||
}
|
||||
} else {
|
||||
if (!table.has(args[i])) {
|
||||
_hash = counter
|
||||
table.set(args[i], counter++)
|
||||
} else {
|
||||
_hash = table.get(args[i])
|
||||
}
|
||||
}
|
||||
key += '@' + _hash
|
||||
}
|
||||
return key
|
||||
}
|
||||
27
src/lib/swr/lib/web-preset.ts
Normal file
27
src/lib/swr/lib/web-preset.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
function isOnline (): boolean {
|
||||
if (typeof navigator.onLine !== 'undefined') {
|
||||
return navigator.onLine
|
||||
}
|
||||
// always assume it's online
|
||||
return true
|
||||
}
|
||||
|
||||
function isDocumentVisible (): boolean {
|
||||
if (
|
||||
typeof document !== 'undefined' &&
|
||||
typeof document.visibilityState !== 'undefined'
|
||||
) {
|
||||
return document.visibilityState !== 'hidden'
|
||||
}
|
||||
// always assume it's visible
|
||||
return true
|
||||
}
|
||||
|
||||
const fetcher = (url: string | Request) => fetch(url).then(res => res.json())
|
||||
|
||||
export default {
|
||||
isOnline,
|
||||
isDocumentVisible,
|
||||
fetcher
|
||||
}
|
||||
|
||||
42
src/lib/swr/types.ts
Normal file
42
src/lib/swr/types.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { Ref, WatchSource } from 'vue'
|
||||
import SWRVCache from './cache'
|
||||
import LocalStorageCache from './cache/adapters/localStorage'
|
||||
|
||||
export type fetcherFn<Data> = (...args: any) => Data | Promise<Data>
|
||||
|
||||
export interface IConfig<
|
||||
Data = any,
|
||||
Fn extends fetcherFn<Data> = fetcherFn<Data>
|
||||
> {
|
||||
refreshInterval?: number
|
||||
cache?: LocalStorageCache | SWRVCache<any>
|
||||
dedupingInterval?: number
|
||||
ttl?: number
|
||||
serverTTL?: number
|
||||
revalidateOnFocus?: boolean
|
||||
revalidateDebounce?: number
|
||||
shouldRetryOnError?: boolean
|
||||
errorRetryInterval?: number
|
||||
errorRetryCount?: number
|
||||
fetcher?: Fn,
|
||||
isOnline?: () => boolean
|
||||
isDocumentVisible?: () => boolean
|
||||
}
|
||||
|
||||
export interface revalidateOptions {
|
||||
shouldRetryOnError?: boolean,
|
||||
errorRetryCount?: number,
|
||||
forceRevalidate?: boolean,
|
||||
}
|
||||
|
||||
export interface IResponse<Data = any, Error = any> {
|
||||
data: Ref<Data | undefined>
|
||||
error: Ref<Error | undefined>
|
||||
isValidating: Ref<boolean>
|
||||
isLoading: Ref<boolean>
|
||||
mutate: (data?: fetcherFn<Data>, opts?: revalidateOptions) => Promise<void>
|
||||
}
|
||||
|
||||
export type keyType = string | any[] | null | undefined
|
||||
|
||||
export type IKey = keyType | WatchSource<keyType>
|
||||
470
src/lib/swr/use-swrv.ts
Normal file
470
src/lib/swr/use-swrv.ts
Normal file
@@ -0,0 +1,470 @@
|
||||
/** ____
|
||||
*--------------/ \.------------------/
|
||||
* / swrv \. / //
|
||||
* / / /\. / //
|
||||
* / _____/ / \. /
|
||||
* / / ____/ . \. /
|
||||
* / \ \_____ \. /
|
||||
* / . \_____ \ \ / //
|
||||
* \ _____/ / ./ / //
|
||||
* \ / _____/ ./ /
|
||||
* \ / / . ./ /
|
||||
* \ / / ./ /
|
||||
* . \/ ./ / //
|
||||
* \ ./ / //
|
||||
* \.. / /
|
||||
* . ||| /
|
||||
* ||| /
|
||||
* . ||| / //
|
||||
* ||| / //
|
||||
* ||| /
|
||||
*/
|
||||
import {
|
||||
reactive,
|
||||
watch,
|
||||
ref,
|
||||
toRefs,
|
||||
// isRef,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
getCurrentInstance,
|
||||
isReadonly,
|
||||
onServerPrefetch,
|
||||
isRef,
|
||||
useSSRContext,
|
||||
type FunctionPlugin,
|
||||
inject
|
||||
} from 'vue'
|
||||
import webPreset from './lib/web-preset'
|
||||
import SWRVCache from './cache'
|
||||
import type { IConfig, IKey, IResponse, fetcherFn, revalidateOptions } from './types'
|
||||
import { tinyassert } from "@hiogawa/utils";
|
||||
|
||||
type StateRef<Data, Error> = {
|
||||
data: Data, error: Error, isValidating: boolean, isLoading: boolean, revalidate: Function, key: any
|
||||
};
|
||||
|
||||
const DATA_CACHE = new SWRVCache<Omit<IResponse, 'mutate'>>()
|
||||
const REF_CACHE = new SWRVCache<StateRef<any, any>[]>()
|
||||
const PROMISES_CACHE = new SWRVCache<Omit<IResponse, 'mutate'>>()
|
||||
|
||||
const defaultConfig: IConfig = {
|
||||
cache: DATA_CACHE,
|
||||
refreshInterval: 0,
|
||||
ttl: 0,
|
||||
serverTTL: 1000,
|
||||
dedupingInterval: 2000,
|
||||
revalidateOnFocus: true,
|
||||
revalidateDebounce: 0,
|
||||
shouldRetryOnError: true,
|
||||
errorRetryInterval: 5000,
|
||||
errorRetryCount: 5,
|
||||
fetcher: webPreset.fetcher,
|
||||
isOnline: webPreset.isOnline,
|
||||
isDocumentVisible: webPreset.isDocumentVisible
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache the refs for later revalidation
|
||||
*/
|
||||
function setRefCache(key: string, theRef: StateRef<any, any>, ttl: number) {
|
||||
const refCacheItem = REF_CACHE.get(key)
|
||||
if (refCacheItem) {
|
||||
refCacheItem.data.push(theRef)
|
||||
} else {
|
||||
// #51 ensures ref cache does not evict too soon
|
||||
const gracePeriod = 5000
|
||||
REF_CACHE.set(key, [theRef], ttl > 0 ? ttl + gracePeriod : ttl)
|
||||
}
|
||||
}
|
||||
|
||||
function onErrorRetry(revalidate: (any: any, opts: revalidateOptions) => void, errorRetryCount: number, config: IConfig): void {
|
||||
if (!(config as any).isDocumentVisible()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (config.errorRetryCount !== undefined && errorRetryCount > config.errorRetryCount) {
|
||||
return
|
||||
}
|
||||
|
||||
const count = Math.min(errorRetryCount || 0, (config as any).errorRetryCount)
|
||||
const timeout = count * (config as any).errorRetryInterval
|
||||
setTimeout(() => {
|
||||
revalidate(null, { errorRetryCount: count + 1, shouldRetryOnError: true })
|
||||
}, timeout)
|
||||
}
|
||||
|
||||
/**
|
||||
* Main mutation function for receiving data from promises to change state and
|
||||
* set data cache
|
||||
*/
|
||||
const mutate = async <Data>(key: string, res: Promise<Data> | Data, cache = DATA_CACHE, ttl = defaultConfig.ttl) => {
|
||||
let data, error, isValidating
|
||||
|
||||
if (isPromise(res)) {
|
||||
try {
|
||||
data = await res
|
||||
} catch (err) {
|
||||
error = err
|
||||
}
|
||||
} else {
|
||||
data = res
|
||||
}
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
isValidating = false
|
||||
|
||||
const newData = { data, error, isValidating }
|
||||
if (typeof data !== 'undefined') {
|
||||
try {
|
||||
cache.set(key, newData, Number(ttl))
|
||||
} catch (err) {
|
||||
console.error('swrv(mutate): failed to set cache', err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revalidate all swrv instances with new data
|
||||
*/
|
||||
const stateRef = REF_CACHE.get(key)
|
||||
if (stateRef && stateRef.data.length) {
|
||||
// This filter fixes #24 race conditions to only update ref data of current
|
||||
// key, while data cache will continue to be updated if revalidation is
|
||||
// fired
|
||||
let refs = stateRef.data.filter(r => r.key === key)
|
||||
|
||||
refs.forEach((r, idx) => {
|
||||
if (typeof newData.data !== 'undefined') {
|
||||
r.data = newData.data
|
||||
}
|
||||
r.error = newData.error
|
||||
r.isValidating = newData.isValidating
|
||||
r.isLoading = newData.isValidating
|
||||
|
||||
const isLast = idx === refs.length - 1
|
||||
if (!isLast) {
|
||||
// Clean up refs that belonged to old keys
|
||||
delete refs[idx]
|
||||
}
|
||||
})
|
||||
|
||||
refs = refs.filter(Boolean)
|
||||
}
|
||||
|
||||
return newData
|
||||
}
|
||||
|
||||
/* Stale-While-Revalidate hook to handle fetching, caching, validation, and more... */
|
||||
function useSWRV<Data = any, Error = any>(
|
||||
key: IKey
|
||||
): IResponse<Data, Error>
|
||||
function useSWRV<Data = any, Error = any>(
|
||||
key: IKey,
|
||||
fn: fetcherFn<Data> | undefined | null,
|
||||
config?: IConfig
|
||||
): IResponse<Data, Error>
|
||||
function useSWRV<Data = any, Error = any>(...args: any[]): IResponse<Data, Error> {
|
||||
const injectedConfig = inject<Partial<IConfig> | null>('swrv-config', null)
|
||||
tinyassert(injectedConfig, 'Injected swrv-config must be an object')
|
||||
let key: IKey
|
||||
let fn: fetcherFn<Data> | undefined | null
|
||||
let config: IConfig = { ...defaultConfig, ...injectedConfig }
|
||||
let unmounted = false
|
||||
let isHydrated = false
|
||||
|
||||
const instance = getCurrentInstance() as any
|
||||
const vm = instance?.proxy || instance // https://github.com/vuejs/composition-api/pull/520
|
||||
if (!vm) {
|
||||
console.error('Could not get current instance, check to make sure that `useSwrv` is declared in the top level of the setup function.')
|
||||
throw new Error('Could not get current instance')
|
||||
}
|
||||
|
||||
const IS_SERVER = typeof window === 'undefined' || false
|
||||
// #region ssr
|
||||
const isSsrHydration = Boolean(
|
||||
!IS_SERVER &&
|
||||
window !== undefined && (window as any).__SSR_STATE__.swrv)
|
||||
// #endregion
|
||||
if (args.length >= 1) {
|
||||
key = args[0]
|
||||
}
|
||||
if (args.length >= 2) {
|
||||
fn = args[1]
|
||||
}
|
||||
if (args.length > 2) {
|
||||
config = {
|
||||
...config,
|
||||
...args[2]
|
||||
}
|
||||
}
|
||||
|
||||
const ttl = IS_SERVER ? config.serverTTL : config.ttl
|
||||
const keyRef = typeof key === 'function' ? (key as any) : ref(key)
|
||||
|
||||
if (typeof fn === 'undefined') {
|
||||
// use the global fetcher
|
||||
fn = config.fetcher
|
||||
}
|
||||
|
||||
let stateRef: StateRef<Data, Error> | null = null
|
||||
// #region ssr
|
||||
if (isSsrHydration) {
|
||||
// component was ssrHydrated, so make the ssr reactive as the initial data
|
||||
|
||||
const swrvState = (window as any).__SSR_STATE__.swrv || []
|
||||
const swrvKey = nanoHex(vm.$.type.__name ?? vm.$.type.name)
|
||||
if (swrvKey !== undefined && swrvKey !== null) {
|
||||
const nodeState = swrvState[swrvKey] || []
|
||||
const instanceState = nodeState[nanoHex(isRef(keyRef) ? keyRef.value : keyRef())]
|
||||
|
||||
if (instanceState) {
|
||||
stateRef = reactive(instanceState)
|
||||
isHydrated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
// #endregion
|
||||
|
||||
if (!stateRef) {
|
||||
stateRef = reactive({
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
isValidating: true,
|
||||
isLoading: true,
|
||||
key: null
|
||||
}) as StateRef<Data, Error>
|
||||
}
|
||||
|
||||
/**
|
||||
* Revalidate the cache, mutate data
|
||||
*/
|
||||
const revalidate = async (data?: fetcherFn<Data>, opts?: revalidateOptions) => {
|
||||
const isFirstFetch = stateRef.data === undefined
|
||||
const keyVal = keyRef.value
|
||||
if (!keyVal) { return }
|
||||
|
||||
const cacheItem = config.cache!.get(keyVal)
|
||||
const newData = cacheItem && cacheItem.data
|
||||
|
||||
stateRef.isValidating = true
|
||||
stateRef.isLoading = !newData
|
||||
if (newData) {
|
||||
stateRef.data = newData.data
|
||||
stateRef.error = newData.error
|
||||
}
|
||||
|
||||
const fetcher = data || fn
|
||||
if (
|
||||
!fetcher ||
|
||||
(!(config as any).isDocumentVisible() && !isFirstFetch) ||
|
||||
(opts?.forceRevalidate !== undefined && !opts?.forceRevalidate)
|
||||
) {
|
||||
stateRef.isValidating = false
|
||||
stateRef.isLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
// Dedupe items that were created in the last interval #76
|
||||
if (cacheItem) {
|
||||
const shouldRevalidate = Boolean(
|
||||
((Date.now() - cacheItem.createdAt) >= (config as any).dedupingInterval) || opts?.forceRevalidate
|
||||
)
|
||||
|
||||
if (!shouldRevalidate) {
|
||||
stateRef.isValidating = false
|
||||
stateRef.isLoading = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const trigger = async () => {
|
||||
const promiseFromCache = PROMISES_CACHE.get(keyVal)
|
||||
if (!promiseFromCache) {
|
||||
const fetcherArgs = Array.isArray(keyVal) ? keyVal : [keyVal]
|
||||
const newPromise = fetcher(...fetcherArgs)
|
||||
PROMISES_CACHE.set(keyVal, newPromise, (config as any).dedupingInterval)
|
||||
await mutate(keyVal, newPromise, (config as any).cache, ttl)
|
||||
} else {
|
||||
await mutate(keyVal, promiseFromCache.data, (config as any).cache, ttl)
|
||||
}
|
||||
stateRef.isValidating = false
|
||||
stateRef.isLoading = false
|
||||
PROMISES_CACHE.delete(keyVal)
|
||||
if (stateRef.error !== undefined) {
|
||||
const shouldRetryOnError = !unmounted && config.shouldRetryOnError && (opts ? opts.shouldRetryOnError : true)
|
||||
if (shouldRetryOnError) {
|
||||
onErrorRetry(revalidate, opts ? Number(opts.errorRetryCount) : 1, config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newData && config.revalidateDebounce) {
|
||||
setTimeout(async () => {
|
||||
if (!unmounted) {
|
||||
await trigger()
|
||||
}
|
||||
}, config.revalidateDebounce)
|
||||
} else {
|
||||
await trigger()
|
||||
}
|
||||
}
|
||||
|
||||
const revalidateCall = async () => revalidate(null as any, { shouldRetryOnError: false })
|
||||
let timer: any = null
|
||||
/**
|
||||
* Setup polling
|
||||
*/
|
||||
onMounted(() => {
|
||||
const tick = async () => {
|
||||
// component might un-mount during revalidate, so do not set a new timeout
|
||||
// if this is the case, but continue to revalidate since promises can't
|
||||
// be cancelled and new hook instances might rely on promise/data cache or
|
||||
// from pre-fetch
|
||||
if (!stateRef.error && (config as any).isOnline()) {
|
||||
// if API request errored, we stop polling in this round
|
||||
// and let the error retry function handle it
|
||||
await revalidate()
|
||||
} else {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
||||
if (config.refreshInterval && !unmounted) {
|
||||
timer = setTimeout(tick, config.refreshInterval)
|
||||
}
|
||||
}
|
||||
|
||||
if (config.refreshInterval) {
|
||||
timer = setTimeout(tick, config.refreshInterval)
|
||||
}
|
||||
if (config.revalidateOnFocus) {
|
||||
document.addEventListener('visibilitychange', revalidateCall, false)
|
||||
window.addEventListener('focus', revalidateCall, false)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Teardown
|
||||
*/
|
||||
onUnmounted(() => {
|
||||
unmounted = true
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
if (config.revalidateOnFocus) {
|
||||
document.removeEventListener('visibilitychange', revalidateCall, false)
|
||||
window.removeEventListener('focus', revalidateCall, false)
|
||||
}
|
||||
const refCacheItem = REF_CACHE.get(keyRef.value)
|
||||
if (refCacheItem) {
|
||||
refCacheItem.data = refCacheItem.data.filter((ref) => ref !== stateRef)
|
||||
}
|
||||
})
|
||||
|
||||
// #region ssr
|
||||
if (IS_SERVER) {
|
||||
const ssrContext = useSSRContext()
|
||||
// make sure srwv exists in ssrContext
|
||||
let swrvRes: Record<string, any> = {}
|
||||
if (ssrContext) {
|
||||
swrvRes = ssrContext.swrv = ssrContext.swrv || swrvRes
|
||||
}
|
||||
|
||||
const ssrKey = nanoHex(vm.$.type.__name ?? vm.$.type.name)
|
||||
// if (!vm.$vnode || (vm.$node && !vm.$node.data)) {
|
||||
// vm.$vnode = {
|
||||
// data: { attrs: { 'data-swrv-key': ssrKey } }
|
||||
// }
|
||||
// }
|
||||
|
||||
// const attrs = (vm.$vnode.data.attrs = vm.$vnode.data.attrs || {})
|
||||
// attrs['data-swrv-key'] = ssrKey
|
||||
// // Nuxt compatibility
|
||||
// if (vm.$ssrContext && vm.$ssrContext.nuxt) {
|
||||
// vm.$ssrContext.nuxt.swrv = swrvRes
|
||||
// }
|
||||
if (ssrContext) {
|
||||
ssrContext.swrv = swrvRes
|
||||
}
|
||||
onServerPrefetch(async () => {
|
||||
await revalidate()
|
||||
if (!swrvRes[ssrKey]) swrvRes[ssrKey] = {}
|
||||
|
||||
swrvRes[ssrKey][nanoHex(keyRef.value)] = {
|
||||
data: stateRef.data,
|
||||
error: stateRef.error,
|
||||
isValidating: stateRef.isValidating
|
||||
}
|
||||
})
|
||||
}
|
||||
// #endregion
|
||||
|
||||
/**
|
||||
* Revalidate when key dependencies change
|
||||
*/
|
||||
try {
|
||||
watch(keyRef, (val) => {
|
||||
if (!isReadonly(keyRef)) {
|
||||
keyRef.value = val
|
||||
}
|
||||
stateRef.key = val
|
||||
stateRef.isValidating = Boolean(val)
|
||||
setRefCache(keyRef.value, stateRef, Number(ttl))
|
||||
|
||||
if (!IS_SERVER && !isHydrated && keyRef.value) {
|
||||
revalidate()
|
||||
}
|
||||
isHydrated = false
|
||||
}, {
|
||||
immediate: true
|
||||
})
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
const res: IResponse = {
|
||||
...toRefs(stateRef),
|
||||
mutate: (data?: fetcherFn<Data>, opts?: revalidateOptions) => revalidate(data, {
|
||||
...opts,
|
||||
forceRevalidate: true
|
||||
})
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
function isPromise<T>(p: any): p is Promise<T> {
|
||||
return p !== null && typeof p === 'object' && typeof p.then === 'function'
|
||||
}
|
||||
|
||||
/**
|
||||
* string to hex 8 chars
|
||||
* @param name string
|
||||
* @returns string
|
||||
*/
|
||||
function nanoHex(name: string): string {
|
||||
try {
|
||||
let hash = 0
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
const chr = name.charCodeAt(i)
|
||||
hash = ((hash << 5) - hash) + chr
|
||||
hash |= 0 // Convert to 32bit integer
|
||||
}
|
||||
let hex = (hash >>> 0).toString(16)
|
||||
while (hex.length < 8) {
|
||||
hex = '0' + hex
|
||||
}
|
||||
return hex
|
||||
} catch {
|
||||
console.error("err name: ", name)
|
||||
return '0000'
|
||||
}
|
||||
}
|
||||
export const vueSWR = (swrvConfig: Partial<IConfig> = defaultConfig): FunctionPlugin => (app) => {
|
||||
app.config.globalProperties.$swrv = useSWRV
|
||||
// app.provide('swrv', useSWRV)
|
||||
app.provide('swrv-config', swrvConfig)
|
||||
}
|
||||
export { mutate }
|
||||
export default useSWRV
|
||||
7
src/lib/utils.ts
Normal file
7
src/lib/utils.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { ClassValue } from "clsx";
|
||||
import { clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
17
src/main.ts
Normal file
17
src/main.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { createHead as CSRHead } from "@unhead/vue/client";
|
||||
import { createHead as SSRHead } from "@unhead/vue/server";
|
||||
import { createSSRApp } from 'vue';
|
||||
import { RouterView } from 'vue-router';
|
||||
import { withErrorBoundary } from './lib/hoc/withErrorBoundary';
|
||||
import { vueSWR } from './lib/swr/use-swrv';
|
||||
import router from './routes';
|
||||
// import { appComponents } from './components'
|
||||
export function createApp() {
|
||||
|
||||
const app = createSSRApp(withErrorBoundary(RouterView))
|
||||
const head = import.meta.env.SSR ? SSRHead() : CSRHead()
|
||||
app.use(head)
|
||||
app.use(vueSWR({revalidateOnFocus: false}))
|
||||
app.use(router)
|
||||
return { app, router, head }
|
||||
}
|
||||
14
src/renderer.tsx
Normal file
14
src/renderer.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { jsxRenderer } from 'hono/jsx-renderer'
|
||||
import { Link, ViteClient } from 'vite-ssr-components/hono'
|
||||
|
||||
export const renderer = jsxRenderer(({ children }) => {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<ViteClient />
|
||||
<Link href="/src/style.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
})
|
||||
7
src/routes/home/Home.vue
Normal file
7
src/routes/home/Home.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div>Home</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
25
src/routes/index.ts
Normal file
25
src/routes/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { type ReactiveHead, type ResolvableValue } from "@unhead/vue";
|
||||
import {
|
||||
createMemoryHistory,
|
||||
createRouter,
|
||||
createWebHistory,
|
||||
type RouteRecordRaw,
|
||||
} from "vue-router";
|
||||
|
||||
type RouteData = RouteRecordRaw & {
|
||||
meta?: ResolvableValue<ReactiveHead>;
|
||||
children?: RouteData[];
|
||||
};
|
||||
const routes: RouteData[] = [
|
||||
{
|
||||
path: "/",
|
||||
component: () => import("./home/Home.vue")
|
||||
},
|
||||
];
|
||||
const router = createRouter({
|
||||
history: import.meta.env.SSR
|
||||
? createMemoryHistory() // server
|
||||
: createWebHistory(), // client
|
||||
routes,
|
||||
});
|
||||
export default router;
|
||||
3
src/style.css
Normal file
3
src/style.css
Normal file
@@ -0,0 +1,3 @@
|
||||
h1 {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
Reference in New Issue
Block a user