-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
231 additions
and
53 deletions.
There are no files selected for viewing
This file contains 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 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 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,108 @@ | ||
import { | ||
AsyncFetch, | ||
type AsyncFetchFunction, | ||
type AsyncFetchCallResult, | ||
} from './AsyncFetch.ts'; | ||
|
||
const EMPTY_ARRAY = Object.freeze([]); | ||
|
||
export interface AsyncFetchCacheGetOptions<_Data = unknown, _Input = unknown> { | ||
key?: unknown | readonly unknown[]; | ||
tags?: readonly string[]; | ||
} | ||
|
||
export class AsyncFetchCache { | ||
private readonly cache: Map<string, AsyncFetchCacheEntry<any, any>>; | ||
private readonly initialCache: Map<string, AsyncFetchCallResult<any, any>>; | ||
|
||
constructor( | ||
initialCache: Iterable< | ||
AsyncFetchCacheEntrySerialization<any> | ||
> = EMPTY_ARRAY, | ||
) { | ||
this.cache = new Map(); | ||
this.initialCache = new Map(); | ||
|
||
for (const [key, value] of initialCache) { | ||
this.initialCache.set(key, value); | ||
} | ||
} | ||
|
||
get = <Data = unknown, Input = unknown>( | ||
fetchFunction: AsyncFetchFunction<Data, Input>, | ||
{ | ||
key: explicitKey, | ||
tags = EMPTY_ARRAY, | ||
}: AsyncFetchCacheGetOptions<Data, Input> = {}, | ||
) => { | ||
const resolvedKey = explicitKey | ||
? JSON.stringify(explicitKey) | ||
: fetchFunction.toString(); | ||
|
||
let cacheEntry = this.cache.get(resolvedKey); | ||
|
||
if (cacheEntry) return cacheEntry; | ||
|
||
cacheEntry = new AsyncFetchCacheEntry<Data, Input>(fetchFunction, { | ||
key: resolvedKey, | ||
tags, | ||
initial: this.initialCache.get(resolvedKey), | ||
}); | ||
|
||
this.cache.set(resolvedKey, cacheEntry); | ||
|
||
return cacheEntry; | ||
}; | ||
|
||
restore(entries: Iterable<AsyncFetchCacheEntrySerialization<any>>) { | ||
for (const [key, value] of entries) { | ||
this.initialCache.set(key, value); | ||
} | ||
} | ||
|
||
serialize(): readonly AsyncFetchCacheEntrySerialization<any>[] { | ||
const serialized: AsyncFetchCacheEntrySerialization<any>[] = []; | ||
|
||
for (const entry of this.cache.values()) { | ||
const serializedEntry = entry.serialize(); | ||
if (serializedEntry) serialized.push(serializedEntry); | ||
} | ||
|
||
return serialized; | ||
} | ||
} | ||
|
||
export class AsyncFetchCacheEntry< | ||
Data = unknown, | ||
Input = unknown, | ||
> extends AsyncFetch<Data, Input> { | ||
readonly key: string; | ||
readonly tags: readonly string[]; | ||
|
||
constructor( | ||
fetchFunction: AsyncFetchFunction<Data, Input>, | ||
{ | ||
key, | ||
initial, | ||
tags = EMPTY_ARRAY, | ||
}: { | ||
key: string; | ||
initial?: AsyncFetchCallResult<Data, Input>; | ||
} & Omit<AsyncFetchCacheGetOptions<Data, Input>, 'key'>, | ||
) { | ||
super(fetchFunction, {initial}); | ||
|
||
this.key = key; | ||
this.tags = tags; | ||
} | ||
|
||
serialize(): AsyncFetchCacheEntrySerialization<Data, Input> | undefined { | ||
const serialized = this.finished?.serialize(); | ||
return serialized && [this.key, serialized]; | ||
} | ||
} | ||
|
||
export type AsyncFetchCacheEntrySerialization< | ||
Data = unknown, | ||
Input = unknown, | ||
> = [key: string, result: AsyncFetchCallResult<Data, Input>]; |
This file contains 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 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 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 |
---|---|---|
@@ -1,26 +1,60 @@ | ||
import type {RenderableProps} from 'preact'; | ||
import {useMemo, useLayoutEffect} from 'preact/hooks'; | ||
import {signal} from '@quilted/preact-signals'; | ||
import {useRef, useLayoutEffect} from 'preact/hooks'; | ||
|
||
import {AsyncHydratedContext} from './context.ts'; | ||
import type {AsyncFetchCache} from '@quilted/async'; | ||
import {signal, type Signal} from '@quilted/preact-signals'; | ||
import {useBrowserDetails} from '@quilted/preact-browser'; | ||
|
||
import {AsyncHydratedContext, AsyncFetchCacheContext} from './context.ts'; | ||
|
||
/** | ||
* Only needed for the following features: | ||
* | ||
* - `<AsyncComponent server={false}>` (needed to correctly hydrate client-only components) | ||
*/ | ||
export function AsyncContext({children}: RenderableProps<{}>) { | ||
const hydrationSignal = useMemo(() => signal(false), []); | ||
export function AsyncContext({ | ||
cache, | ||
children, | ||
}: RenderableProps<{ | ||
cache: AsyncFetchCache; | ||
}>) { | ||
const browser = useBrowserDetails(); | ||
const internals = useRef<{ | ||
hydrated: Signal<boolean>; | ||
deserialized: boolean; | ||
}>({deserialized: false} as any); | ||
if (internals.current.hydrated == null) { | ||
Object.assign(internals.current, {hydrated: signal(false)}); | ||
} | ||
|
||
const {hydrated, deserialized} = internals.current; | ||
|
||
if (typeof document === 'object') { | ||
if (!deserialized) { | ||
const serialization = browser.serializations.get('quilt:fetch:hydrated'); | ||
if (Array.isArray(serialization)) cache.restore(serialization); | ||
|
||
internals.current.deserialized = true; | ||
} | ||
|
||
useLayoutEffect(() => { | ||
hydrationSignal.value = true; | ||
hydrated.value = true; | ||
}, []); | ||
} else { | ||
browser.serializations.set('quilt:fetch:cache', () => cache.serialize()); | ||
} | ||
|
||
return ( | ||
<AsyncHydratedContext.Provider value={hydrationSignal}> | ||
const content = ( | ||
<AsyncHydratedContext.Provider value={hydrated}> | ||
{children} | ||
</AsyncHydratedContext.Provider> | ||
); | ||
|
||
return cache ? ( | ||
<AsyncFetchCacheContext.Provider value={cache}> | ||
{content} | ||
</AsyncFetchCacheContext.Provider> | ||
) : ( | ||
content | ||
); | ||
} |
This file contains 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 |
---|---|---|
@@ -1,4 +1,7 @@ | ||
import type {AsyncFetchCache} from '@quilted/async'; | ||
import type {ReadonlySignal} from '@quilted/preact-signals'; | ||
import {createOptionalContext} from '@quilted/preact-context'; | ||
import type {Signal} from '@quilted/preact-signals'; | ||
|
||
export const AsyncHydratedContext = createOptionalContext<Signal<boolean>>(); | ||
export const AsyncHydratedContext = | ||
createOptionalContext<ReadonlySignal<boolean>>(); | ||
export const AsyncFetchCacheContext = createOptionalContext<AsyncFetchCache>(); |
This file was deleted.
Oops, something went wrong.
This file contains 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,44 @@ | ||
import {useRef} from 'preact/hooks'; | ||
|
||
import {AsyncFetch} from '@quilted/async'; | ||
import type { | ||
AsyncFetchCacheGetOptions, | ||
AsyncFetchFunction, | ||
} from '@quilted/async'; | ||
import {useComputed} from '@quilted/preact-signals'; | ||
|
||
import {AsyncFetchCacheContext} from '../context.ts'; | ||
|
||
export const useAsyncFetchCache = AsyncFetchCacheContext.use; | ||
|
||
export async function useAsyncFetch<Data, Input>( | ||
fetchFunction: AsyncFetchFunction<Data, Input>, | ||
options?: AsyncFetchCacheGetOptions<Data, Input>, | ||
) { | ||
const functionRef = useRef(fetchFunction); | ||
functionRef.current = fetchFunction; | ||
|
||
const fetchCache = useAsyncFetchCache({optional: true}); | ||
|
||
const fetchSignal = useComputed(() => { | ||
const resolvedFetchFunction: AsyncFetchFunction<Data, Input> = (...args) => | ||
functionRef.current(...args); | ||
|
||
if (!fetchCache) { | ||
return new AsyncFetch<Data, Input>(resolvedFetchFunction); | ||
} | ||
|
||
// TODO: react to key changes | ||
return fetchCache.get(resolvedFetchFunction, options); | ||
}, [fetchCache]); | ||
|
||
const fetch = fetchSignal.value; | ||
|
||
if (fetch.status === 'pending') { | ||
console.log(fetch.status, fetch.isRunning); | ||
if (fetch.isRunning) throw fetch.promise; | ||
throw fetch.call(); | ||
} | ||
|
||
return fetch; | ||
} |
This file contains 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